useReducer 是态管态管 useState 的替代方案,用来处理复杂的理u理状态或逻辑。当与其它 Hooks(useContext)结合使用,现全有时也是局状一个好的选择,不需要引入一些第三方状态管理库,态管态管例如 Redux、理u理Mobx。现全 目标在本文结束时,局状您将了解: Context API 的态管态管使用。在哪些场景下可以使用 Context 而不是理u理类似于 Redux 这些第三方的状态管理库。如何使用 useState + useContext 实现暗黑模式切换。现全如何使用 useReducer + useContext 实现 todos。局状什么是态管态管 Context?Context 解决了跨组件之间的通信,也是理u理官方默认提供的一个方案,无需引入第三方库,现全适用于存储对组件树而言是全局的数据状态,并且不要有频繁的数据更新。例如:主题、当前认证的亿华云用户、首选语言。 使用 React.createContext 方法创建一个上下文,该方法接收一个参数做为其默认值,返回 MyContext.Provider、MyContext.Consumer React 组件。 const MyContext = React.createContext(defaultValue);MyContext.Provider 组件接收 value 属性用于传递给子组件(使用 MyContext.Consumer 消费的组件),无论嵌套多深都可以接收到。 {children} </MyContext.Provider>将我们的内容包装在 MyContext.Consumer 组件中,以便订阅 context 的变更,类组件中通常会这样写。 {value => {value}}} </MyContext.Consumer>以上引入不必要的代码嵌套也增加了代码的复杂性,React Hooks 提供的 useContext 使得访问上下文状态变得更简单。 const App = () => { const value = useContext(newContext); console.log(value); // this will return { color: black } return }以上我们对 Context 做一个简单了解,更多内容参考官网 Context、useContext 文档描述,下面我们通过两个例子来学习如何使用 useContext 管理全局状态。 useState + useContext 主题切换本节的第一个示例是使用 React hooks 的 useState 和 useContext API 实现暗黑主题切换。 实现 Context 的 Provider在 ThemeContext 组件中我们定义主题为 light、dark。定义 ThemeProvider 在上下文维护两个属性:当前选择的主题 theme、切换主题的站群服务器函数 toggleTheme()。 通过 useContext hook 可以在其它组件中获取到 ThemeProvider 维护的两个属性,在使用 useContext 时需要确保传入 React.createContext 创建的对象,在这里我们可以自定义一个 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 { children } } export const useTheme = () => { const context = useContext(ThemeContext); return context; }; 创建一个 AppProviders,用来组装创建的多个上下文。代码位置:src/contexts/index.js。 import { ThemeProvider } from ./ThemeContext; const AppProviders = ({ children }) => { return { children } } export default AppProviders;实现 ToggleTheme 组件在 App.js 文件中,将 AppProviders 组件做为根组件放在最顶层,这样被包裹的组件都可以使用 AppProviders 组件提供的属性。 代码位置:src/App.js。 import AppProviders from ./contexts; import ToggleTheme from ./components/ToggleTheme; import ./App.css; const App = () => ( ); export default App; 在 ToggleTheme 组件中,我们使用自定义的 useTheme hook 访问 theme 对象和 toggleTheme 函数,以下创建了一个简单主题切换,用来设置背景颜色和文字颜色。 代码位置:src/components/ToggleTheme.jsx。 import { useTheme } from ../contexts/ThemeContext const ToggleTheme = () => { const { theme, toggleTheme } = useTheme(); return backgroundColor: theme.background, color: theme.color, width: 100%, height: 100vh, textAlign: center, }}> Toggling Light/Dark Theme Toggling Light/Dark Theme in React with useState and useContext Switch to { theme.type } mode } export default ToggleTheme;Demo 演示
视频 示例代码地址:https://github.com/qufei1993/react-state-example/tree/usestate-usecontext-theme。 useReducer + useContext 实现 Todos使用 useReducer 和 useContext 完成一个 Todos。这个例子很简单,可以帮助我们学习如何实现一个简单的状态管理工具,类似 Redux 这样可以跨组件共享数据状态。 reducer 实现在 src/reducers 目录下实现 reducer 需要的逻辑,定义的 initialState 变量、香港云服务器reducer 函数都是为 useReducer 这个 Hook 做准备的,在这个地方需要都导出下,reducer 函数是一个纯函数,了解 Redux 的小伙伴对这个概念应该不陌生。 // 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 跨组件数据共享定义 TodoContext 导出 state、dispatch,结合 useContext 自定义一个 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 { children } } export const useTodo = () => { const context = useContext(TodoContext); return context; };// src/contexts/index.js import { TodoProvider } from ./TodoContext; const AppProviders = ({ children }) => { return { children } } export default AppProviders;实现 Todos 组件在 TodoAdd、Todo、Todos 三个组件内分别都可以通过 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 setContent(e.target.value)} /> dispatch({ type: TODO_LIST_ADD, payload: { content } }) }}> 添加 }; const Todo = ({ todo }) => { console.log(Todo render); const { dispatch } = useTodo(); const [isEdit, setIsEdit] = useState(false); const [content, setContent] = useState(todo.content); return { !isEdit ? <> {todo.content} : <> setContent(e.target.value) } /> setIsEdit(false); dispatch({ type: TODO_LIST_EDIT, payload: { id: todo.id, content } }) }}> 更新 } } const Todos = () => { console.log(Todos render); const { state } = useTodo(); return Todos App useReducer + useContent 实现 todos { state.todos.map(todo => ) } } export default Todos;Demo 演示上面代码实现需求是没问题,但是存在一个性能问题,如果 Context 中的某个熟悉发生变化,所有依赖该 Context 的组件也会被重新渲染,观看以下视频演示: 
视频 示例代码地址:https://github.com/qufei1993/react-state-example/tree/usereducer-usecontext-todos。 Context 小结useState/useReducer 管理的是组件的状态,如果子组件想获取根组件的状态一种简单的做法是通过 Props 层层传递,另外一种是把需要传递的数据封装进 Context 的 Provider 中,子组件通过 useContext 获取来实现全局状态共享。 Context 对于构建小型应用程序时,相较于 Redux,实现起来会更容易且不需要依赖第三方库,同时还要看下适用场景。在官网也有说明,适用于存储对组件树而言是全局的数据状态,并且不要有频繁的数据更新(例如:主题、当前认证的用户、首选语言)。 以下是使用 Context 会遇到的几个问题: Context 中的某个属性一旦变化,所有依赖该 Context 的组件也都会重新渲染,尽管对组件做了 React.memo() 或 shouldComponentUpdate() 优化,还是会触发强制更新。过多的 context 如何维护?因为子组件需要被 Context.Provider 包裹才能获取到上下文的值,过多的 Context,例如 ... 是不是有点之前 “callback 回调地狱” 的意思了。这里有个解决思路是创建一个 store container,参考 The best practice to combine containers to have it as "global" state、Apps with many containers。provider 父组件重新渲染可能导致 consumers 组件的意外渲染问题,参考 Context 注意事项。在我们实际的 React 项目中没有一个 Hook 或 API 能解决我们所有的问题,根据应用程序的大小和架构来选择适合于您的方法是最重要的。 介绍完 React 官方提供的状态管理工具外,下一节介绍一下社区状态管理界的 “老大哥 Redux”。 文末阅读原文查看文中两个示例代码! Referencehttps://blog.logrocket.com/guide-to-react-usereducer-hook/https://zh-hans.reactjs.org/docs/context.html |