React 狀態(tài)管理 - useState/useReducer + useContext 實(shí)現(xiàn)全局狀態(tài)管理
useReducer 是 useState 的替代方案,用來處理復(fù)雜的狀態(tài)或邏輯。當(dāng)與其它 Hooks(useContext)結(jié)合使用,有時(shí)也是一個(gè)好的選擇,不需要引入一些第三方狀態(tài)管理庫,例如 Redux、Mobx。
目標(biāo)
在本文結(jié)束時(shí),您將了解:
- Context API 的使用。
- 在哪些場景下可以使用 Context 而不是類似于 Redux 這些第三方的狀態(tài)管理庫。
- 如何使用 useState + useContext 實(shí)現(xiàn)暗黑模式切換。
- 如何使用 useReducer + useContext 實(shí)現(xiàn) todos。
什么是 Context?
Context 解決了跨組件之間的通信,也是官方默認(rèn)提供的一個(gè)方案,無需引入第三方庫,適用于存儲(chǔ)對(duì)組件樹而言是全局的數(shù)據(jù)狀態(tài),并且不要有頻繁的數(shù)據(jù)更新。例如:主題、當(dāng)前認(rèn)證的用戶、首選語言。
使用 React.createContext 方法創(chuàng)建一個(gè)上下文,該方法接收一個(gè)參數(shù)做為其默認(rèn)值,返回 MyContext.Provider、MyContext.Consumer React 組件。
const MyContext = React.createContext(defaultValue);
MyContext.Provider 組件接收 value 屬性用于傳遞給子組件(使用 MyContext.Consumer 消費(fèi)的組件),無論嵌套多深都可以接收到。
<MyContext.Provider value={color: 'blue'}>
{children}
</MyContext.Provider>
將我們的內(nèi)容包裝在 MyContext.Consumer 組件中,以便訂閱 context 的變更,類組件中通常會(huì)這樣寫。
<MyContext.Consumer>
{value => <span>{value}</span>}}
</MyContext.Consumer>
以上引入不必要的代碼嵌套也增加了代碼的復(fù)雜性,React Hooks 提供的 useContext 使得訪問上下文狀態(tài)變得更簡單。
const App = () => {
const value = useContext(newContext);
console.log(value); // this will return { color: 'black' }
return <div></div>
}
以上我們對(duì) Context 做一個(gè)簡單了解,更多內(nèi)容參考官網(wǎng) Context、useContext 文檔描述,下面我們通過兩個(gè)例子來學(xué)習(xí)如何使用 useContext 管理全局狀態(tài)。
useState + useContext 主題切換
本節(jié)的第一個(gè)示例是使用 React hooks 的 useState 和 useContext API 實(shí)現(xiàn)暗黑主題切換。
實(shí)現(xiàn) Context 的 Provider
在 ThemeContext 組件中我們定義主題為 light、dark。定義 ThemeProvider 在上下文維護(hù)兩個(gè)屬性:當(dāng)前選擇的主題 theme、切換主題的函數(shù) toggleTheme()。
通過 useContext hook 可以在其它組件中獲取到 ThemeProvider 維護(hù)的兩個(gè)屬性,在使用 useContext 時(shí)需要確保傳入 React.createContext 創(chuàng)建的對(duì)象,在這里我們可以自定義一個(gè) hook useTheme 便于在其它組件中直接使用。
代碼位置:src/contexts/ThemeContext.js。
import React, { useState, useContext } from "react";
export const themes = {
light: {
type: 'light',
background: '#ffffff',
color: '#000000',
},
dark: {
type: 'dark',
background: '#000000',
color: '#ffffff',
},
};
const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {},
});
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState(themes.dark);
const context = {
theme,
toggleTheme: () => setTheme(theme === themes.dark
? themes.light
: themes.dark)
}
return <ThemeContext.Provider value={context}>
{ children }
</ThemeContext.Provider>
}
export const useTheme = () => {
const context = useContext(ThemeContext);
return context;
};
創(chuàng)建一個(gè) AppProviders,用來組裝創(chuàng)建的多個(gè)上下文。代碼位置:src/contexts/index.js。
import { ThemeProvider } from './ThemeContext';
const AppProviders = ({ children }) => {
return <ThemeProvider>
{ children }
</ThemeProvider>
}
export default AppProviders;
實(shí)現(xiàn) ToggleTheme 組件
在 App.js 文件中,將 AppProviders 組件做為根組件放在最頂層,這樣被包裹的組件都可以使用 AppProviders 組件提供的屬性。
代碼位置:src/App.js。
import AppProviders from './contexts';
import ToggleTheme from './components/ToggleTheme';
import './App.css';
const App = () => (
<AppProviders>
<ToggleTheme />
</AppProviders>
);
export default App;
在 ToggleTheme 組件中,我們使用自定義的 useTheme hook 訪問 theme 對(duì)象和 toggleTheme 函數(shù),以下創(chuàng)建了一個(gè)簡單主題切換,用來設(shè)置背景顏色和文字顏色。
代碼位置:src/components/ToggleTheme.jsx。
import { useTheme } from '../contexts/ThemeContext'
const ToggleTheme = () => {
const { theme, toggleTheme } = useTheme();
return <div style={{
backgroundColor: theme.background,
color: theme.color,
width: '100%',
height: '100vh',
textAlign: 'center',
}}>
<h2 className="theme-title"> Toggling Light/Dark Theme </h2>
<p className="theme-desc"> Toggling Light/Dark Theme in React with useState and useContext </p>
<button className="theme-btn" onClick={toggleTheme}>
Switch to { theme.type } mode
</button>
</div>
}
export default ToggleTheme;
Demo 演示
??視頻??
示例代碼地址:https://github.com/qufei1993/react-state-example/tree/usestate-usecontext-theme。
useReducer + useContext 實(shí)現(xiàn) Todos
使用 useReducer 和 useContext 完成一個(gè) Todos。這個(gè)例子很簡單,可以幫助我們學(xué)習(xí)如何實(shí)現(xiàn)一個(gè)簡單的狀態(tài)管理工具,類似 Redux 這樣可以跨組件共享數(shù)據(jù)狀態(tài)。
reducer 實(shí)現(xiàn)
在 src/reducers 目錄下實(shí)現(xiàn) reducer 需要的邏輯,定義的 initialState 變量、reducer 函數(shù)都是為 useReducer 這個(gè) Hook 做準(zhǔn)備的,在這個(gè)地方需要都導(dǎo)出下,reducer 函數(shù)是一個(gè)純函數(shù),了解 Redux 的小伙伴對(duì)這個(gè)概念應(yīng)該不陌生。
// src/reducers/todos-reducer.jsx
export const TODO_LIST_ADD = 'TODO_LIST_ADD';
export const TODO_LIST_EDIT = 'TODO_LIST_EDIT';
export const TODO_LIST_REMOVE = 'TODO_LIST_REMOVE';
const randomID = () => Math.floor(Math.random() * 10000);
export const initialState = {
todos: [{ id: randomID(), content: 'todo list' }],
};
const reducer = (state, action) => {
switch (action.type) {
case TODO_LIST_ADD: {
const newTodo = {
id: randomID(),
content: action.payload.content
};
return {
todos: [ ...state.todos, newTodo ],
}
}
case TODO_LIST_EDIT: {
return {
todos: state.todos.map(item => {
const newTodo = { ...item };
if (item.id === action.payload.id) {
newTodo.content = action.payload.content;
}
return newTodo;
})
}
}
case TODO_LIST_REMOVE: {
return {
todos: state.todos.filter(item => item.id !== action.payload.id),
}
}
default: return state;
}
}
export default reducer;
Context 跨組件數(shù)據(jù)共享
定義 TodoContext 導(dǎo)出 state、dispatch,結(jié)合 useContext 自定義一個(gè) useTodo hook 獲取信息。
// src/contexts/TodoContext.js
import React, { useReducer, useContext } from "react";
import reducer, { initialState } from "../reducers/todos-reducer";
const TodoContext = React.createContext(null);
export const TodoProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const context = {
state,
dispatch
}
return <TodoContext.Provider value={context}>
{ children }
</TodoContext.Provider>
}
export const useTodo = () => {
const context = useContext(TodoContext);
return context;
};
// src/contexts/index.js
import { TodoProvider } from './TodoContext';
const AppProviders = ({ children }) => {
return <TodoProvider>
{ children }
</TodoProvider>
}
export default AppProviders;
實(shí)現(xiàn) Todos 組件
在 TodoAdd、Todo、Todos 三個(gè)組件內(nèi)分別都可以通過 useTodo() hook 獲取到 state、dispatch。
import { useState } from "react";
import { useTodo } from "../../contexts/TodoContext";
import { TODO_LIST_ADD, TODO_LIST_EDIT, TODO_LIST_REMOVE } from "../../reducers/todos-reducer";
const TodoAdd = () => {
console.log('TodoAdd render');
const [content, setContent] = useState('');
const { dispatch } = useTodo();
return <div className="todo-add">
<input className="input" type="text" onChange={e => setContent(e.target.value)} />
<button className="btn btn-lg" onClick={() => {
dispatch({ type: TODO_LIST_ADD, payload: { content } })
}}>
添加
</button>
</div>
};
const Todo = ({ todo }) => {
console.log('Todo render');
const { dispatch } = useTodo();
const [isEdit, setIsEdit] = useState(false);
const [content, setContent] = useState(todo.content);
return <div className="todo-list-item">
{
!isEdit ? <>
<div className="todo-list-item-content">{todo.content}</div>
<button className="btn" onClick={() => setIsEdit(true)}> 編輯 </button>
<button className="btn" onClick={() => dispatch({ type: TODO_LIST_REMOVE, payload: { id: todo.id } })}> 刪除 </button>
</> : <>
<div className="todo-list-item-content">
<input className="input" value={content} type="text" onChange={ e => setContent(e.target.value) } />
</div>
<button className="btn" onClick={() => {
setIsEdit(false);
dispatch({ type: TODO_LIST_EDIT, payload: { id: todo.id, content } })
}}> 更新 </button>
<button className="btn" onClick={() => setIsEdit(false)}> 取消 </button>
</>
}
</div>
}
const Todos = () => {
console.log('Todos render');
const { state } = useTodo();
return <div className="todos">
<h2 className="todos-title"> Todos App </h2>
<p className="todos-desc"> useReducer + useContent 實(shí)現(xiàn) todos </p>
<TodoAdd />
<div className="todo-list">
{
state.todos.map(todo => <Todo key={todo.id} todo={todo} />)
}
</div>
</div>
}
export default Todos;
Demo 演示
上面代碼實(shí)現(xiàn)需求是沒問題,但是存在一個(gè)性能問題,如果 Context 中的某個(gè)熟悉發(fā)生變化,所有依賴該 Context 的組件也會(huì)被重新渲染,觀看以下視頻演示:
??視頻??
示例代碼地址:https://github.com/qufei1993/react-state-example/tree/usereducer-usecontext-todos。
Context 小結(jié)
useState/useReducer 管理的是組件的狀態(tài),如果子組件想獲取根組件的狀態(tài)一種簡單的做法是通過 Props 層層傳遞,另外一種是把需要傳遞的數(shù)據(jù)封裝進(jìn) Context 的 Provider 中,子組件通過 useContext 獲取來實(shí)現(xiàn)全局狀態(tài)共享。
Context 對(duì)于構(gòu)建小型應(yīng)用程序時(shí),相較于 Redux,實(shí)現(xiàn)起來會(huì)更容易且不需要依賴第三方庫,同時(shí)還要看下適用場景。在官網(wǎng)也有說明,適用于存儲(chǔ)對(duì)組件樹而言是全局的數(shù)據(jù)狀態(tài),并且不要有頻繁的數(shù)據(jù)更新(例如:主題、當(dāng)前認(rèn)證的用戶、首選語言)。
以下是使用 Context 會(huì)遇到的幾個(gè)問題:
- Context 中的某個(gè)屬性一旦變化,所有依賴該 Context 的組件也都會(huì)重新渲染,盡管對(duì)組件做了 React.memo() 或 shouldComponentUpdate() 優(yōu)化,還是會(huì)觸發(fā)強(qiáng)制更新。
- 過多的 context 如何維護(hù)?因?yàn)樽咏M件需要被 Context.Provider 包裹才能獲取到上下文的值,過多的 Context,例如 ... 是不是有點(diǎn)之前 “callback 回調(diào)地獄” 的意思了。這里有個(gè)解決思路是創(chuàng)建一個(gè) store container,參考 The best practice to combine containers to have it as "global" state、Apps with many containers。
- provider 父組件重新渲染可能導(dǎo)致 consumers 組件的意外渲染問題,參考 Context 注意事項(xiàng)。
在我們實(shí)際的 React 項(xiàng)目中沒有一個(gè) Hook 或 API 能解決我們所有的問題,根據(jù)應(yīng)用程序的大小和架構(gòu)來選擇適合于您的方法是最重要的。
介紹完 React 官方提供的狀態(tài)管理工具外,下一節(jié)介紹一下社區(qū)狀態(tài)管理界的 “老大哥 Redux”。
文末閱讀原文查看文中兩個(gè)示例代碼!
- Referencehttps://blog.logrocket.com/guide-to-react-usereducer-hook/
- https://zh-hans.reactjs.org/docs/context.html