写在前面
看本篇博客的前提需要了解 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.todoListcase 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.todoListcase 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.filterdefault: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 的逻辑实现,具体实现可看源码。
源码
