写在前面
看本篇博客的前提需要了解 Redux 是什么,若不知请移步 Redux
自从 React Hooks 推出 useReducer Hook 来,在使用 useReducer Hook 的时候其实可以明显感觉到就是和 Redux 是差不多的,都是以 reducer 和 action 两个主要概念为主。
reducer 是一个 (state, action) => newState 的状态产生机,action 是一个动作描述对象。
只不过对于 state 的读写接口的处理方式不同,Redux 是通过 createStore(reducer, initialState)
来创建一个 store 实例,该实例封装了 state 的读写接口和监听接口:getState 、dispatch、subscribe
,各组件通过调用 store 实例提供的状态操作接口来对状态进行使用和操作。
但 useReducer Hook 是没有使用 store 实例,而是遵循 Hook 总是返回读写接口的规则,直接通过 [state, dispatch] = useReducer(reducer, initialState)
的方式返回状态的读写接口。在 Redux 中,store.dispatch
触发事件动作时,Redux 并不会为我们主动重新渲染视图,而是需要我们调用 store.subscribe
在监听函数中手动 render 视图。但 Hook 一般是在调用写接口后就会自动重新 render 视图。因此,useReducer Hook 就是这样的,dispatch 写接口调用后就帮我们自动重新 render 了。
那么如何让创建 reducer 的读写 API 的组件将状态的读写 API:state 和 dispatch 应用到其所有的后代组件呢?
像 Redux 中创建的 store 还可以通过 import store 的方式使用到,但是 useReducer 只能在函数组件内部使用得到应用状态读写 API,更不可能导出去了。此时就用到了 useContext() 这个 Hook。
下面以用 useReducer 代替 Redux 做一个 todo-list demo,来讲解 useReducer + useContext 是如何代替 Redux 的。
目录结构如下:
但这种代替方式只适用于组件都是函数组件的情况
1. 使用 useReducer 创建状态机
const [state, dispatch] = useReducer(reducer, {
filter: filterOptions.SHOW_ALL,
todoList: []
});
2. 使用 createContext 和 useContext 暴露状态机接口
2.1 createContext
context.js(因为创建的 context 会在各个组件中使用 useContext 得到,因此需要单独文件导出)
import {createContext} from 'react';
const Context = createContext(null);
export default Context
App.js(设置 context 的作用范围)
function App() {
const [state, dispatch] = useReducer(reducer, {
filter: filterOptions.SHOW_ALL,
todoList: []
});
return (
<Context.Provider value={{ state, dispatch }}>
<div className="App">
我是 APP,要点:useReducer 的初始值不要传 null,要初始化,否则使用 ajax fetch 不成功
<AddTodo/>
<TodoList/>
<Filter/>
</div>
</Context.Provider>
);
}
2.2 useContext
TodoList / index.js
const TodoList = () => {
const {state, dispatch} = useContext(Context);
useEffect(()=> {
fetchTodoList(dispatch)
},[])
const getVisibleTodoList = (state, filter)=>{
switch (filter) {
case filterOptions.SHOW_ALL:
return state.todoList
case filterOptions.SHOW_COMPLETE:
return state.todoList.filter(todo => todo.isComplete)
case filterOptions.SHOW_UNCOMPLETE:
return state.todoList.filter(todo => !todo.isComplete)
}
}
return state.todoList.length > 0 ? (
<ul>
{getVisibleTodoList(state, state.filter).map((todo, index) => (
<li key={index} onClick={() => dispatch(toggleTodo(index))}
style={{textDecoration: todo.isComplete ? 'line-through' : 'none'}}>{todo.text}</li>
))}
</ul>
) : (<div>加载中...</div>);
};
3. 使用最原始的拆分方式代替 combineReducers
Redux 中有提供 combineReducers 合并 reducer 的方法,在 useReducer Hook 中,我们可以使用最原始的对象拆发的方法代替 combineReducers
reducers / todoList.js
import {ADD_TODO, INIT_TODOS, TOGGLE_TODO} from '../constants/actionTypes';
const todoList = (state, action)=>{
switch (action.type) {
case INIT_TODOS:
return action.todoList
case TOGGLE_TODO:
return state.map((todo, index)=>{
if(index === action.index)
return {...todo, isComplete: !todo.isComplete}
return todo
})
case ADD_TODO:
return [...state, { text: action.text, isComplete: false}]
default:
return state
}
}
export default todoList
reducers / filter.js
import {SET_FILTER} from '../constants/actionTypes';
const filter = (state, action)=>{
switch (action.type) {
case SET_FILTER:
return action.filter
default:
return state
}
}
export default filter
reducers / indes.js
import todoList from './todoList';
import filter from './filter';
const reducer = (state, action)=>{
return {
todoList: todoList(state.todoList, action),
filter: filter(state.filter, action)
}
}
export default reducer
源码链接
以上内容只是在讲如何使用 useReducer 和 useContext 代替 Redux,因此并没有细细讲 todo-list 的逻辑实现,具体实现可看源码。