使用 useReducer 和 Context 进行状态管理入门
为你的 React 应用选择状态管理库可能比较棘手。你可以选择以下库:
- 将 React 的
useReducer
hook 与 React Context 结合使用 - 选择像Redux或MobX这样长期流行的库
- 尝试一些新的东西,比如react-sweet-state或Recoil(如果你喜欢冒险的话!)
为了帮助您做出更明智的决定,本系列旨在快速概述如何使用各种状态管理解决方案创建待办事项列表应用程序。
在这篇文章中,我们将使用useReducer
hook 和 React Context 的组合来构建我们的示例应用程序,并快速绕道查看一个名为React Tracked 的库。
如果您想继续,我已经在react-state-comparison为本指南中创建的示例应用程序创建了一个存储库。
这篇文章假设您了解如何在 React 中呈现功能组件,以及对钩子如何工作的一般理解。
应用程序功能和结构
我们将在此应用程序中实现的功能包括以下内容:
- 编辑待办事项列表的名称
- 创建、删除和编辑任务
应用程序的结构将如下所示:
src
common
components # component code we can re-use in future posts
react # the example app we are creating in today's post
state # where we initialise and manage our state
components # state-aware components that make use of our common components
创建我们的通用组件
首先,我们将在common
文件夹中创建一些组件。这些“视图”组件并不知道我们使用了什么状态管理库。它们的唯一用途是渲染组件,并使用我们作为 props 传入的回调函数。我们将它们放在一个公共文件夹中,以便在本系列的后续文章中重复使用它们。
我们需要四个组件:
NameView
- 允许我们编辑待办事项列表名称的字段CreateTaskView
- 带有“创建”按钮的字段,以便我们可以创建新任务TaskView
- 复选框、任务名称和任务的“删除”按钮TasksView
- 循环并渲染所有任务
例如,Name
组件的代码如下所示:
// src/common/components/name
import React from 'react';
const NameView = ({ name, onSetName }) => (
<input
type="text"
defaultValue={name}
onChange={(event) => onSetName(event.target.value)}
/>
);
export default NameView;
每次我们编辑名称时,我们都会onSetName
使用输入的当前值(通过event
对象访问)调用回调。
在实际应用中,您可能会考虑等到用户保存了任务名称后再进行此调用。您可以为此添加一个“保存”按钮,或者监听用户点击离开输入字段或按下 Enter 键离开输入字段的动作。
其他三个组件的代码遵循类似的模式,您可以在common/components文件夹中查看。
定义商店的形状
接下来我们应该考虑一下store应该是什么样子。本地状态是指状态存储在各个 React 组件中。而store则是一个中心位置,你可以将应用的所有状态都集中存放在这里。
我们将存储待办事项列表的名称,以及包含所有任务及其 ID 映射的任务图:
const store = {
listName: 'To-do list name',
tasks: {
'1': {
name: 'Task name',
checked: false,
id: 1,
}
}
}
创建我们的 reducer 和 actions
我们使用 Reducer 和 Actions 来修改存储中的数据。
action的作用是请求修改 store。它会说:
“嘿,我想将待办事项列表的名称改为‘新奇特的名字’”。
Reducer的任务是修改 store。Reducer收到请求后,会执行如下操作:
“好的,我会将待办事项列表的名称改为‘新奇特的名字’”
行动
每个动作都有两个值:
- 一个动作
type
- 更新列表的名称,你可以将类型定义为updateListName
- 一个动作
payload
——更新列表的名称,有效载荷将包含“Fancy new name”
调度我们的updateListName
行动看起来是这样的:
dispatch({
type: 'updateListName',
payload: { name: 'Fancy new name' }
});
Reducers
Reducer 是我们定义如何使用 action 的有效负载来修改状态的地方。它是一个函数,第一个参数是 store 的当前状态,第二个参数是 action:
// src/react/state/reducers
export const reducer = (state, action) => {
const { listName, tasks } = state;
switch (action.type) {
case 'updateListName': {
const { name } = action.payload;
return { listName: name, tasks };
}
default: {
return state;
}
}
};
使用 switch 语句,reducer 会尝试为 action 找到匹配的 case。如果该 action 未在 reducer 中定义,我们将进入该default
case 并返回state
未更改的对象。
如果已定义,我们将继续返回state
对象的修改版本。在我们的例子中,我们将更改其listName
值。
这里要注意的一件非常重要的事情是,我们永远不会直接修改我们收到的状态对象。例如,不要这样做:
state.listName = 'New list name';
当 store 中的值发生变化时,我们需要重新渲染应用,但如果直接修改状态对象,则不会发生这种情况。我们需要确保返回的是新对象。如果您不想手动执行此操作,可以使用像immer这样的库来安全地为您完成此操作。
创建并初始化我们的商店
现在我们已经定义了 reducer 和 action,我们需要使用 React Context 创建 store useReducer
:
// src/react/state/store
import React, { createContext, useReducer } from 'react';
import { reducer } from '../reducers';
import { initialState } from '../../../common/mocks';
export const TasksContext = createContext();
export const TasksProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<TasksContext.Provider value={{ state, dispatch }}>
{children}
</TasksContext.Provider>
);
};
这个useReducer
hook 允许我们使用之前定义的 reducer 函数来创建一个 reducer。我们还传入一个初始状态对象,它可能看起来像这样:
const initialState = {
listName: 'My new list',
tasks: {},
};
当我们将 Provider 包装在我们的应用程序中时,任何组件都将能够访问该state
对象以呈现其所需内容,以及dispatch
在用户与 UI 交互时分派操作的功能。
使用 Provider 包装我们的应用
我们需要在我们的src/react/components
文件夹中创建我们的 React 应用程序,并将其包装在我们的新提供程序中:
// src/react/components
import React from 'react';
import { TasksProvider } from '../state/store';
import Name from './name';
import Tasks from './tasks';
import CreateTask from './create-task';
const ReactApp = () => (
<>
<h2>React with useReducer + Context</h2>
<TasksProvider>
<Name />
<Tasks />
<CreateTask />
</TasksProvider>
</>
);
export default ReactApp;
您可以看到我们在这里使用的所有状态感知组件,我将介绍Name
下面的组件。
访问数据和调度操作
使用NameView
我们之前创建的组件,我们将重用它创建我们的Name
组件。它可以使用钩子从 Context 访问值useContext
:
import React, { useContext } from 'react';
import NameView from '../../../common/components/name';
import { TasksContext } from '../../state/store';
const Name = () => {
const {
dispatch,
state: { listName }
} = useContext(TasksContext);
const onSetName = (name) =>
dispatch({ type: 'updateListName', payload: { name } });
return <NameView name={name} onSetName={onSetName} />;
};
export default Name;
我们可以使用state
值来渲染列表的名称,并dispatch
使用函数在名称被编辑时触发 action。然后我们的 reducer 就会更新 store。就是这么简单!
React Context 的问题
不幸的是,这种简单性也带来了一个问题。使用 React Context 会导致任何使用该useContext
钩子的组件重新渲染。在我们的示例中,我们在和组件useContext
中都使用了钩子。如果我们修改列表的名称,就会导致组件重新渲染,反之亦然。Name
Tasks
Tasks
对于我们这个小型的待办事项列表应用来说,这不会造成任何性能问题,但随着应用规模的扩大,大量的重新渲染对性能不利。如果您希望轻松使用 React Context 和 useReducer,又不想遇到重新渲染的问题,可以使用一个替代库。
使用 React Tracked 替换 React Context
React Tracked是一个超小(1.6kB)库,可作为 React Context 之上的包装器。
您的 Reducer 和 Actions 文件可以保持不变,但您需要store
用以下内容替换您的文件:
//src/react-tracked/state/store
import React, { useReducer } from 'react';
import { createContainer } from 'react-tracked';
import { reducer } from '../reducers';
const useValue = ({ reducer, initialState }) =>
useReducer(reducer, initialState);
const { Provider, useTracked, useTrackedState, useUpdate } = createContainer(
useValue
);
export const TasksProvider = ({ children, initialState }) => (
<Provider reducer={reducer} initialState={initialState}>
{children}
</Provider>
);
export { useTracked, useTrackedState, useUpdate };
有三个钩子可以用来访问状态和调度值:
const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();
这就是唯一的区别!现在,如果您编辑列表名称,它不会导致任务重新渲染。
结论
与 React Context 结合使用useReducer
是快速开始管理状态的好方法。然而,使用 Context 时,重新渲染可能会成为一个问题。如果您正在寻找快速解决方案,可以使用 React Tracked 这个简洁的小库。
要查看我们今天介绍的任何代码,您可以前往react-state-comparison查看完整示例。您还可以预览我们下周将要讲解的 Redux 示例应用!如果您有任何疑问,或者对我应该参考的状态管理库有什么建议,请告诉我。
感谢阅读!
文章来源:https://dev.to/emma/getting-started-with-state-management-using-usereducer-and-context-4a6k