写在前面

看本篇博客的前提需要了解 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 的。

目录结构如下:
用 useReducer 代替 Redux - 图1

但这种代替方式只适用于组件都是函数组件的情况

1. 使用 useReducer 创建状态机

  1. const [state, dispatch] = useReducer(reducer, {
  2. filter: filterOptions.SHOW_ALL,
  3. todoList: []
  4. });

2. 使用 createContext 和 useContext 暴露状态机接口

2.1 createContext

context.js(因为创建的 context 会在各个组件中使用 useContext 得到,因此需要单独文件导出)

  1. import {createContext} from 'react';
  2. const Context = createContext(null);
  3. export default Context

App.js(设置 context 的作用范围)

  1. function App() {
  2. const [state, dispatch] = useReducer(reducer, {
  3. filter: filterOptions.SHOW_ALL,
  4. todoList: []
  5. });
  6. return (
  7. <Context.Provider value={{ state, dispatch }}>
  8. <div className="App">
  9. 我是 APP,要点:useReducer 的初始值不要传 null,要初始化,否则使用 ajax fetch 不成功
  10. <AddTodo/>
  11. <TodoList/>
  12. <Filter/>
  13. </div>
  14. </Context.Provider>
  15. );
  16. }

2.2 useContext

TodoList / index.js

  1. const TodoList = () => {
  2. const {state, dispatch} = useContext(Context);
  3. useEffect(()=> {
  4. fetchTodoList(dispatch)
  5. },[])
  6. const getVisibleTodoList = (state, filter)=>{
  7. switch (filter) {
  8. case filterOptions.SHOW_ALL:
  9. return state.todoList
  10. case filterOptions.SHOW_COMPLETE:
  11. return state.todoList.filter(todo => todo.isComplete)
  12. case filterOptions.SHOW_UNCOMPLETE:
  13. return state.todoList.filter(todo => !todo.isComplete)
  14. }
  15. }
  16. return state.todoList.length > 0 ? (
  17. <ul>
  18. {getVisibleTodoList(state, state.filter).map((todo, index) => (
  19. <li key={index} onClick={() => dispatch(toggleTodo(index))}
  20. style={{textDecoration: todo.isComplete ? 'line-through' : 'none'}}>{todo.text}</li>
  21. ))}
  22. </ul>
  23. ) : (<div>加载中...</div>);
  24. };

3. 使用最原始的拆分方式代替 combineReducers

Redux 中有提供 combineReducers 合并 reducer 的方法,在 useReducer Hook 中,我们可以使用最原始的对象拆发的方法代替 combineReducers

reducers / todoList.js

  1. import {ADD_TODO, INIT_TODOS, TOGGLE_TODO} from '../constants/actionTypes';
  2. const todoList = (state, action)=>{
  3. switch (action.type) {
  4. case INIT_TODOS:
  5. return action.todoList
  6. case TOGGLE_TODO:
  7. return state.map((todo, index)=>{
  8. if(index === action.index)
  9. return {...todo, isComplete: !todo.isComplete}
  10. return todo
  11. })
  12. case ADD_TODO:
  13. return [...state, { text: action.text, isComplete: false}]
  14. default:
  15. return state
  16. }
  17. }
  18. export default todoList

reducers / filter.js

  1. import {SET_FILTER} from '../constants/actionTypes';
  2. const filter = (state, action)=>{
  3. switch (action.type) {
  4. case SET_FILTER:
  5. return action.filter
  6. default:
  7. return state
  8. }
  9. }
  10. export default filter

reducers / indes.js

  1. import todoList from './todoList';
  2. import filter from './filter';
  3. const reducer = (state, action)=>{
  4. return {
  5. todoList: todoList(state.todoList, action),
  6. filter: filter(state.filter, action)
  7. }
  8. }
  9. export default reducer

源码链接

以上内容只是在讲如何使用 useReducer 和 useContext 代替 Redux,因此并没有细细讲 todo-list 的逻辑实现,具体实现可看源码。
源码