使用 useReducer 和 Context 进行状态管理入门

2025-06-05

使用 useReducer 和 Context 进行状态管理入门

为你的 React 应用选择状态管理库可能比较棘手。你可以选择以下库:

为了帮助您做出更明智的决定,本系列旨在快速概述如何使用各种状态管理解决方案创建待办事项列表应用程序。

在这篇文章中,我们将使用useReducerhook 和 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
Enter fullscreen mode Exit fullscreen mode

创建我们的通用组件

首先,我们将在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;
Enter fullscreen mode Exit fullscreen mode

每次我们编辑名称时,我们都会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,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

创建我们的 reducer 和 actions

我们使用 Reducer 和 Actions 来修改存储中的数据。

action作用是请求修改 store。它会说:

“嘿,我想将待办事项列表的名称改为‘新奇特的名字’”。

Reducer任务是修改 store。Reducer收到请求后,会执行如下操作

“好的,我会将待办事项列表的名称改为‘新奇特的名字’”

行动

每个动作都有两个值:

  • 一个动作type- 更新列表的名称,你可以将类型定义为updateListName
  • 一个动作payload——更新列表的名称,有效载荷将包含“Fancy new name”

调度我们的updateListName行动看起来是这样的:

dispatch({ 
    type: 'updateListName', 
    payload: { name: 'Fancy new name' } 
});
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
};

Enter fullscreen mode Exit fullscreen mode

使用 switch 语句,reducer 会尝试为 action 找到匹配的 case。如果该 action 未在 reducer 中定义,我们将进入该defaultcase 并返回state未更改的对象。

如果已定义,我们将继续返回state对象的修改版本。在我们的例子中,我们将更改其listName值。

这里要注意的一件非常重要的事情是,我们永远不会直接修改我们收到的状态对象。例如,不要这样做:

state.listName = 'New list name';
Enter fullscreen mode Exit fullscreen mode

当 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>
    );
};

Enter fullscreen mode Exit fullscreen mode

这个useReducerhook 允许我们使用之前定义的 reducer 函数来创建一个 reducer。我们还传入一个初始状态对象,它可能看起来像这样:

const initialState = {
  listName: 'My new list',
  tasks: {},
};
Enter fullscreen mode Exit fullscreen mode

当我们将 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;

Enter fullscreen mode Exit fullscreen mode

您可以看到我们在这里使用的所有状态感知组件,我将介绍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;
Enter fullscreen mode Exit fullscreen mode

我们可以使用state值来渲染列表的名称,并dispatch使用函数在名称被编辑时触发 action。然后我们的 reducer 就会更新 store。就是这么简单!

React Context 的问题

不幸的是,这种简单性也带来了一个问题。使用 React Context 会导致任何使用该useContext钩子的组件重新渲染。在我们的示例中,我们在和组件useContext中都使用了钩子。如果我们修改列表的名称,就会导致组件重新渲染,反之亦然。NameTasksTasks

对于我们这个小型的待办事项列表应用来说,这不会造成任何性能问题,但随着应用规模的扩大,大量的重新渲染对性能不利。如果您希望轻松使用 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 };

Enter fullscreen mode Exit fullscreen mode

有三个钩子可以用来访问状态和调度值:

const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();
Enter fullscreen mode Exit fullscreen mode

这就是唯一的区别!现在,如果您编辑列表名称,它不会导致任务重新渲染。

结论

与 React Context 结合使用useReducer是快速开始管理状态的好方法。然而,使用 Context 时,重新渲染可能会成为一个问题。如果您正在寻找快速解决方案,可以使用 React Tracked 这个简洁的小库。

要查看我们今天介绍的任何代码,您可以前往react-state-comparison查看完整示例。您还可以预览我们下周将要讲解的 Redux 示例应用!如果您有任何疑问,或者对我应该参考的状态管理库有什么建议,请告诉我。

感谢阅读!

文章来源:https://dev.to/emma/getting-started-with-state-management-using-usereducer-and-context-4a6k
PREV
React 中的 styled-components 入门
NEXT
如何利用 2020 年最新的软件开发趋势提升你的游戏水平:人工智能 (AI) 和机器学习 (ML) 日益受到关注;物联网 (IoT) 需求不断增长;区块链强势反弹!渐进式 Web 应用程序 (PWA) 成为新的潮流;交付后客户服务至关重要!一些流行的开发实践