React 状态管理 - useState/useReducer + useContext 实现全局状态管理

开发 前端
架构

useReducer 是 useState 的替代方案,用来处理复杂的状态或逻辑。当与其它 Hooks(useContext)结合使用,有时也是一个好的选择,不需要引入一些第三方状态管理库,例如 Redux、Mobx。

目标

在本文结束时,您将了解:

  • Context API 的使用。
  • 在哪些场景下可以使用 Context 而不是类似于 Redux 这些第三方的状态管理库。
  • 如何使用 useState + useContext 实现暗黑模式切换。
  • 如何使用 useReducer + useContext 实现 todos。

什么是 Context?

Context 解决了跨组件之间的通信,也是官方默认提供的一个方案,无需引入第三方库,适用于存储对组件树而言是全局的数据状态,并且不要有频繁的数据更新。例如:主题、当前认证的用户、首选语言。

使用 React.createContext 方法创建一个上下文,该方法接收一个参数做为其默认值,返回 MyContext.Provider、MyContext.Consumer React 组件。

const MyContext = React.createContext(defaultValue);

MyContext.Provider 组件接收 value 属性用于传递给子组件(使用 MyContext.Consumer 消费的组件),无论嵌套多深都可以接收到。

<MyContext.Provider value={color: 'blue'}>
{children}
</MyContext.Provider>

将我们的内容包装在 MyContext.Consumer 组件中,以便订阅 context 的变更,类组件中通常会这样写。

<MyContext.Consumer>
{value => <span>{value}</span>}}
</MyContext.Consumer>

以上引入不必要的代码嵌套也增加了代码的复杂性,React Hooks 提供的 useContext 使得访问上下文状态变得更简单。

const App = () => {
const value = useContext(newContext);
console.log(value); // this will return { color: 'black' }

return <div></div>
}

以上我们对 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 <ThemeContext.Provider value={context}>
{ children }
</ThemeContext.Provider>
}

export const useTheme = () => {
const context = useContext(ThemeContext);
return context;
};

创建一个 AppProviders,用来组装创建的多个上下文。代码位置:src/contexts/index.js。


import { ThemeProvider } from './ThemeContext';

const AppProviders = ({ children }) => {
return <ThemeProvider>
{ children }
</ThemeProvider>
}
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 = () => (
<AppProviders>
<ToggleTheme />
</AppProviders>
);

export default App;

在 ToggleTheme 组件中,我们使用自定义的 useTheme hook 访问 theme 对象和 toggleTheme 函数,以下创建了一个简单主题切换,用来设置背景颜色和文字颜色。

代码位置: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 实现 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 <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;

实现 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 <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 实现 todos </p>
<TodoAdd />
<div className="todo-list">
{
state.todos.map(todo => <Todo key={todo.id} todo={todo} />)
}
</div>
</div>
}

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
责任编辑:武晓燕 来源: 编程界
相关推荐

2022-03-18 14:09:52

ReactJavaScript

2021-09-14 05:32:49

React 前端 组件

2024-04-22 09:12:39

Redux开源React

2022-11-10 08:02:08

2024-04-26 07:54:07

ZustandReact状态管理库

2020-10-15 06:28:08

React 5管理库状态

2024-01-08 09:36:47

管理库代码

2020-10-09 11:50:10

ReactRecoil前端

2020-11-13 15:40:18

React前端Recoil

2023-04-10 07:26:28

UseStateUseReducer

2021-09-28 09:00:00

开发JavaScript存储

2020-09-17 06:42:31

ReactStoreon前端

2024-04-18 08:33:09

React状态管理组件组合

2021-08-14 08:45:27

React开发应用程序

2022-06-20 09:01:50

SwiftUI状态管理系统

2022-04-16 12:38:39

CSS前端

2019-10-08 11:10:18

React自动保存前端

2024-04-17 07:59:26

React状态管理属性钻取

2021-06-03 09:31:56

React状态模式

2010-09-09 08:33:00

点赞
收藏

51CTO技术栈公众号