TodoMVC 与 Redux
TodoMVC
TodoMVC是一个示例项目,它使用目前流行的不同JavaScript框架的来实现同一个Demo,来帮助你熟悉和选择最合适的前端框架。
怎样快速学习和了解一门JavaScript框架或学习使用新框架的特性? 我的经验是做一个TodoMVC应用吧
TodoMVC 应用功能点
- 数据的 CRUD
- 数据模型的管理
- MVC的理论实践
- 组件拆分
- 状态管理
TodoMVC
是一个功能和UI都十分完备的教学示例,实乃学习框架之必备🌰
Redux 简介
Redux
是遵循 Flux
模式的一种实现,是一个状态管理库,适用于 React
,Angular
,VueJs
等框架或库,而不是局限于某一特定UI库。
Redux 核心概念
+----------+
| | +-----------+ sliceState,action +------------+
| Action |dispacth(action)| +--------------------------------> |
| +----------------> store | | Reducers |
| Creators | | <--------------------------------+ |
| | +-----+-----+ (state, action) => state +------------+
+-----^----+ |
| |
| |subscribe
| |
| |
| |
| +--------+ |
+--------------+ View <---+
+--------+
更新View的方式
dispatch
触发action
action
携带方法名type
, 和数据payload
到store
(期间可能进行异步数据处理)store
接收到type
和payload
交给相应的reducer
reducer
找到对应的方法更新state
Store、Action 与 Reducer
Store
是整个 Redux 应用的状态容器,是一个对象Action
也是一个对象,表明事件,需要有 type 字段Reducer
是一个函数,会根据不同 Action 来决定返回不同的数据
目标
- 应用TodoMVC UI
- 全部使用函数式组件
- 使用TypeScript
- 学习使用Hooks(useEffect、useContext、useRef、useMemo、useState)
- 模拟Redux
需求分析
新增Todo
- 在输入框填写新增的值,按Enter键新增并清空输入框
删除Todo
- 点击删除“按钮x”删除选择数据
- 点击右下角“clear complete”清空已
complete
的数据
修改Todo
- 双击某行数据时切换到编辑状态并自动获得焦点
- 按回车键或光标离开可以修改数据
查询Todo
- 当list有值时,可以正常显示数据
- 左下角“item left”根据当前item数自动变化
- 点击下方的
all
active
complete
可以过滤list,并且高亮自动能切换 - 勾选item可以切换
active
和complete
样式 - 当item全选时,新增数据框左侧的“小箭头”高亮
- 点击新增数据框左侧的“小箭头”,可以全选或全不选item
- 进入首页时,新增输入框自动获得焦点
安装 TodoMVC
模块
npm install todomvc-common todomvc-app-css
import 'todomvc-common/base.css';
import 'todomvc-app-css/index.css';
直接使用vue版TodoMVC模板,去掉vue相关代码,保留结构
使用 todomvc
UI, 可以免去样式和dom结构编写,让我们专注于使用JS框架快速实现业务逻辑
组件拆分
页面基本结构
// App.tsx
const Input: SFC<any> = () => {...}
const TodoItem: SFC<ITodoItemProps> = {...}
const TodoList: SFC<any> = () => {...}
const Footer: SFC<any> = () => {...}
function TodoAPP() {
return (
<section className="todoapp">
<header className="header">
<h1>todos</h1>
<Input />
</header>
<TodoList />
<Footer />
</section>
);
}
Store
确定应用状态和事件方法
应用全局状态
const initialState: IAPPState = {
todos: todoStore.list,
newTodo: '',
editTodo: '',
visibility: ShowType.ALL,
};
这里状态的划分为全局状态和局部状态,局部状态由组件内部管理,后面会说到,所以TodoMVC 的全局状态基本就是这4个就够了,在实际项目应用中,全局状态尽量少,要做到每个状态都是全局必须的,避免在全局存放过多的状态,可能会引起组件不必要的更新。
事件方法
export enum ActionType {
// 新增Todo
CREATE = 'create',
// 更新Todo
UPDATE = 'update',
// 删除Todo
DELETE = 'delete',
// 删除状态是已完成的Tdo
REMOVE_COMPLETED = 'removeCompleted',
// 设置当前编辑的Todo
EDIT_SET = 'setEdit',
// 改变当前显示类型
CHANGE_SHOW_TYPE = 'changeShowType',
// 更新当前编辑的Todo
UPDATE_EDIT_TODO = 'updateEditTodo',
// 全部切换为 完成/未完成
TOGGLE_ALL = 'toggleAll',
}
使用 createContext
创建 Context
组件
使用 useReducer 管理复杂的 state
useReducer
接收一个形如 (state, action) => newState
的 reducer
,并返回当前的 state
以及与其配套的 dispatch
方法
使用 <Context.Provider>
做为父组件的组件,将在 state
更新时,触发更新
// Store.tsx
const initialState: IAPPState = {
todos: todoStore.list,
newTodo: '',
editTodo: '',
visibility: ShowType.ALL,
};
const reducer = (state: IAPPState, action: IAction): IAPPState => {
const { type, payload } = action;
return methods[type] ? methods[type](state, payload) : state;
};
const Context = React.createContext({
state: initialState,
dispatch: (() => 0) as React.Dispatch<IAction>,
});
const Provider: SFC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return <Context.Provider value={{ state, dispatch}}>{children}</Context.Provider>;
};
export { Provider, Context };
上面的 methods
是具体更新 state
的方法对象
使用Store
Provider
的子组件可以从 Context
中获取全局的 state
与 dispatch
// App.tsx
import { Provider, Context } from './Store'
function TodoAPP() {
return (
<Provider>
<section className="todoapp">
...
</section>
</Provider>
);
}
Input 输入组件
使用 useContext 接收 Store
中的 Context
对象,并返回 state
和 dispatch
当组件上层最近的 <Context.Provider>
更新时,该 hook 会触发组件更新,并使用最新的 state
调用了 useContext
的组件总会在 context
值变化时重新渲染
使用 useState 创建组件内部状态 title
, 该状态在组件内部使用或销毁,并不影响全局state, 接收一个初始状态的初始值和更新状态的函数 changeTitle
使用 dispatch
触发一个 action
操作,创建一个 todo
const Input: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
// 内部状态 title
const [title, changeTitle] = useState<string>('');
// 输入时更新 title
function onChange(e: ChangeEvent<HTMLInputElement>) {
changeTitle(e.target.value.trim());
}
// 回车新增一条Todo
function onKeyDown(e: KeyboardEvent<HTMLInputElement>) {
// 新增
dispatch({
type: ActionType.CREATE,
payload: { id: utils.uuid(), title, completed: false },
});
// 清空
changeTitle('');
}
return (
<input
className="new-todo"
autoFocus
autoComplete="off"
placeholder="What needs to be done?"
value={title}
onChange={onChange}
onKeyDown={onKeyDown}
/>
);
};
TodoItem 组件
editing
是属于每一个 TodoItem
的内部状态
使用 useRef 来返回一个可变的 ref 引用对象,其 .current
属性被初始化为传入的参数,常用于访问 DOM对象
,这里用来使用编辑输入框获取焦点
useEffect 告诉组件需要在渲染后执行某些操作。React
会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM
更新之后调用它, 这里传入第二个参数 [editing]
做为依赖,以优化 effect
执行次数,只有当 editing
改变时,才执行
const TodoItem: SFC<ITodoItemProps> = ({ completed, title, index }) => {
const [editing, changeEditing] = useState<boolean>(false);
const iptRef = useRef(null);
const { state, dispatch } = useContext(Context);
const { editTodo } = state;
// DOM更新,获取焦点
useEffect(() => (editing && iptRef.current.focus()), [editing]);
// 双击编辑
function onDoubleClick() {...}
// 失焦,更新Todo
function onEditBlur() {...}
// 回车,更新Todo
function onEditEnter(e: KeyboardEvent<HTMLInputElement>) {...}
// 更新当前编辑的Todo
function onEditChange(e: ChangeEvent<HTMLInputElement>) {...}
// 切换完成状态
function onToggleComplete(e: ChangeEvent<HTMLInputElement>) {...}
// 删除
function onDestroy() {
return (
<li>
...
</li>
)
}
Footer 组件
使用 useMemo 获取类似Vue中的 计算属性,避免在每次渲染时都进行高开销的计算,传入 todos
, 当 todos
更新时才重新计算
const Footer: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
const { todos, visibility } = state;
// 计算属性:正在进行中的数量、已完成的数量、提示文字
const { activedNum, completedNum, activeTodoWord } = useMemo(() => {...})
// 改变显示类型
function onChangeShowType(type: ShowType) {...}
// 清空已完成的Todo
function onClearCompleted() {...}
return (
<footer className="footer">
...
</footer>
)
}
TodoList 组件
const TodoList: SFC<any> = () => {
const { state, dispatch } = useContext(Context);
const { todos, visibility } = state;
const activedNum = useMemo(() => utils.filterTodos(todos, ShowType.ACTIVE).length, [todos]);
const todoList = useMemo(() =>utils.filterTodos(todos, visibility), [todos, visibility]);
function onToggleAll(e: ChangeEvent<HTMLInputElement>) {...}
return (
<section className="main">
<input
id="toggle-all"
className="toggle-all"
type="checkbox"
onChange={onToggleAll}
checked={activedNum === 0}
/>
<label htmlFor="toggle-all">Mark all as complete</label>
<ul className="todo-list">
{todoList.map((v, i) => {
return <TodoItem key={v.id} completed={v.completed} title={v.title} index={i} />;
})}
</ul>
</section>
)
}
为了防止文章过长,并没有粘贴所有代码,主要说明 React
中的一些 hooks
在 TodoMVC
中的一些实际应用场景,以便于我们更容易更快速的理解和掌握hooks的用法
优化
在我们公司的实际应用中,并没有使用 redux
来管理应用状态,而是使用 mobx
,在实际开发中我更倾向局部状态与全局状态分而治之,在后台管理系统中,全局的状态并不多,无非就是一些用户信息、菜单状态和数据、消息等等,一般以页面划分 store
,复杂的组件有自己的 store
, store
中包含了所有状态和操作方法。
Redux 的缺点
- 样板代码管理比较分散,需要建立
actionTypes
、actions
、reducers
- 多人协作时就会出现名字冲突,相似业务的流程重复
dispatch
需要引用actions
或要记忆Action Type
等问题
优化 dispatch
在上面的 hooks 实现的伪 redux
中, dispatch
方法使用仍然比较繁琐,可以优化下面两点
- UI逻辑处理时,不用引用 ActionsTypes
- 直接调用 Actions, 并有方法提示
import { ActionsTypes } from './types'
import { Context } from './store'
// 现在的调用方式
const {state, dispatch} = useContext(Context)
dispatch({
type: ActionTypes.TOGGLE_ALL,
payload: { completed },
});
// 优化后的调用方式
const {state, actions} = useContext(Context)
actions.toggleAll({ completed })
这里的优化实现主要在 Provider
的实现中, 只要把 Actions
方法挂载到 Context
中就可以
const Provider: SFC<any> = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
// 生成 Actions
type IActions = { [name in ActionType]?: (data?: Record<string, any>) => void };
let actions: IActions = {};
for (let i in ActionType) {
actions[ActionType[i] as ActionType] = data =>
dispatch({ type: ActionType[i] as ActionType, payload: data });
}
// Context 挂载 state、dispatch、actions
return <Context.Provider value={{ state, dispatch, actions }}>{children}</Context.Provider>;
};
Vue
相较于 React
有一个优点就是 Vue
的包比 React
要小,在 React
中使用 setState
来管理应用状态,相比于 Vue
,代码写法上稍显繁琐,引用第三方的库如 Redux
或 mobx
无疑又会增加React的体积,hooks
的出现给了我们以希望,在一些相对简单的应用中完全可以代替 mobx
或 redux
。但是同样也要小心使用,没有了生命周期的控制,使用不当也容易造成组件重复多次渲染产生性能瓶颈,所以在使用 hooks
时,要多思考怎样使用更合理,怎样才不会产生性能问题。