寫在前面
看本篇博客的前提需要了解 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 的邏輯實現,具體實現可看源碼。