完美的 React 状态管理:useReducer 和 Context API

2025-06-07

完美的 React 状态管理:useReducer 和 Context API

✨ 简介

向 React 应用添加状态可能很棘手,尤其是在应用规模不断增长的情况下。您应该在哪个层级管理状态?哪些组件只提供本地状态?那些需要在任何地方访问的状态该怎么办?Redux 是一个很棒的状态管理库,但对于中小型应用来说,它可能有些过度,因为您可能需要经常使用这类应用。

在本教程中,我们将构建一个小型用户管理应用程序,它将教您如何以我认为目前最好的方式管理 React 中的状态。

🎯 目标

  • 设置 React 应用
  • 100% 使用 React Hooks
  • 使用 Context API
  • 使用 useReducer 钩子
  • 将 API 数据异步加载到我们的状态中
  • 添加主题切换开关

📺 我们将做什么

点击此处查看应用的实际运行情况。
点击 hero 查看 GitHub 仓库。

🔨 设置应用程序

让我们首先使用 create-react-app 创建一个新的 React 应用程序:
npx create-react-app user-management

我喜欢在我的 React 项目中使用 Material UI 或 Tailwind,这次我们使用 Material UI:
npm install @material-ui/core

并在我们的index.html中添加Roboto字体:
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />

以及添加Material UI的图标集:
npm install @material-ui/icons

然后让我们删除除 index.js 和 App.js 之外的所有文件,因为我们不会使用它们。

现在我们将创建一个基本的布局和仪表盘,用于保存用户列表。我们将每个页面包装在一个_layout.js提供主题和模板的函数中。在函数内部,App.js我们将添加标准的 react-router 功能:

_layout.js

import { Box, Container, CssBaseline } from "@material-ui/core";
import React, { useState } from "react";

export default function Layout({ children }) {
  return (
    <>
      <CssBaseline />
      <Container maxWidth="md">
        <Box marginTop={2}>{children}</Box>
      </Container>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

App.js

import { Route, BrowserRouter as Router, Switch } from "react-router-dom";

import Layout from "./_layout";
import Users from "./Users";

function App() {
  return (
    <Layout>
      <Router>
        <Switch>
          <Route path="/">
            <Users />
          </Route>
          <Route path="/edit-user">
            <h1>Edit user</h1>
          </Route>
        </Switch>
      </Router>
    </Layout>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

用户.js

import {
  Button,
  Divider,
  Grid,
  Paper,
  Typography,
  makeStyles,
} from "@material-ui/core";

import Brightness7Icon from "@material-ui/icons/Brightness7";
import React from "react";
import UserList from "./UserList";

const useStyles = makeStyles((theme) => ({
  paper: {
    padding: theme.spacing(4),
    margin: "auto",
  },
  img: {
    width: "100%",
  },
  divider: {
    marginBottom: theme.spacing(2),
  },
}));

export default function Users() {
  const classes = useStyles();

  return (
    <Paper className={classes.paper}>
      <Grid container justify="space-between" alignItems="start">
        <Grid item>
          <Typography gutterBottom variant="h4">
            Users
          </Typography>
        </Grid>
        <Grid item>
          <Grid container spacing={4} alignItems="center">
            <Grid item>
              <Button variant="contained" color="primary">
                Load users
              </Button>
            </Grid>
            <Grid item>
              <Brightness7Icon />
            </Grid>
          </Grid>
        </Grid>
      </Grid>
      <Divider className={classes.divider} />
      <UserList />
    </Paper>
  );
}

Enter fullscreen mode Exit fullscreen mode

此外,我已经为我们稍后将进行的主题切换添加了默认图标。

添加用户列表

现在让我们添加保存用户信息的卡片。

用户列表.js

import { Grid } from "@material-ui/core";
import React from "react";
import User from "./User";

export default function UserList() {
  const users = [1, 2, 3];

  return (
    <Grid container spacing={2}>
      {users.map((user, i) => (
        <Grid item xs={12} sm={6}>
          <User key={i} user={user} />
        </Grid>
      ))}
    </Grid>
  );
}
Enter fullscreen mode Exit fullscreen mode

用户.js

import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import React from "react";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles({
  media: {
    height: 140,
  },
});

export default function User() {
  const classes = useStyles();

  return (
    <Card className={classes.root}>
      <CardActionArea>
        <CardContent>
          <Typography gutterBottom variant="h5" component="h2">
            Clementine Bauch
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>ID: </strong> Samantha
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>Username: </strong> Samantha
          </Typography>
          <Typography variant="body2" color="textSecondary" component="p">
            <strong>Email: </strong> Nathan@yesenia.net
          </Typography>
        </CardContent>
      </CardActionArea>
      <CardActions>
        <Button size="small" variant="contained" color="secondary">
          Delete
        </Button>
        <Button size="small" variant="contained" color="primary">
          Edit
        </Button>
      </CardActions>
    </Card>
  );
}
Enter fullscreen mode Exit fullscreen mode

我们目前只使用了一些硬编码的用户数据和一个包含 3 个元素的数组来显示用户信息。在下一节中,我们将通过 API 加载用户信息并将其存储在应用状态中。

在此之前,让我们首先使用 Context API 创建主题切换。

💡 添加 Context API

创建一个名为 的新文件夹context,并在此处添加一个名为 的文件夹theme。在此文件夹中,我们将创建以下 3 个文件:context.jsindex.jsreducer.js

我将逐步解释每个文件。

context.js
我们将使用 React 的 Context API 来包装我们的应用程序,并提供一些我们想要提供的值,在本例中是主题设置。

首先,我们将创建一个新的上下文”

const { createContext } = require("react");
const ThemeContext = createContext();`
Enter fullscreen mode Exit fullscreen mode

然后我们将设置一个包装函数,为整个应用程序提供主题:

<ThemeContext.Provider theme={currentTheme} setTheme={setTheme}>
      {children}
</ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

为了使其与 Material UI 兼容,我们必须将主题传递给它们的createMuiTheme()函数。我们将使用 React 的 useState 钩子来获取和设置状态。我们还将把该setTheme函数提供给我们的上下文。

我们可以通过钩子在应用程序的任何地方使用上下文值useContext()
export const useTheme = () => useContext(ThemeContext);

整个上下文看起来是这样的:

import React, { useContext } from "react";

import { createMuiTheme } from "@material-ui/core";

const { createContext } = require("react");

const ThemeContext = createContext();

export const useTheme = () => useContext(ThemeContext);

export const ThemeProvider = ({ children }) => {
  const dark = {
    palette: {
      type: "dark",
    },
  };

  const currentTheme = createMuiTheme(dark);

  return (
    <ThemeContext.Provider value={currentTheme}>
      {children}
    </ThemeContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

接下来我们将使用index.js轻松将上下文文件导入其他文件:

index.js

import { useTheme, ThemeProvider } from "./context";

export { useTheme, ThemeProvider };
Enter fullscreen mode Exit fullscreen mode

我们将App.js使用提供程序将我们的应用程序包装在里面:

App.js

...
function App() {
  return (
    <ThemeProvider>
         ...
    </ThemeProvider>
  );
}
...
Enter fullscreen mode Exit fullscreen mode

我们将更新_layout.js文件,以便我们可以为我们的主题提供材料 UI:

_layout.js

import {
  Box,
  Container,
  CssBaseline,
  ThemeProvider,
  createMuiTheme,
} from "@material-ui/core";

import React from "react";
import { useThemeState } from "./context/theme";

export const light = {
  palette: {
    type: "light",
  },
};

export const dark = {
  palette: {
    type: "dark",
  },
};

export default function Layout({ children }) {
  const { theme } = useThemeState();

  const lightTheme = createMuiTheme(light);
  const darkTheme = createMuiTheme(dark);

  return (
    <ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
      <CssBaseline />
      <Container maxWidth="md">
        <Box marginTop={2}>{children}</Box>
      </Container>
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

现在,我们可以通过useTheme()钩子在应用的任何地方使用该主题。例如,Users.js我们可以添加以下内容,根据主题设置显示太阳或月亮:

const theme = useTheme();
{theme.palette.type === "light" ? 
<Brightness7Icon /> : <Brightness4Icon />}
Enter fullscreen mode Exit fullscreen mode

这非常有用,我们为应用添加了一个全局状态!但是如果我们想更新该状态怎么办?这时 useReducer 就派上用场了。

添加 useReducer

React useReducer hook 是 useState 的替代方案。它接受一个修改状态对象的函数和一个初始状态对象作为参数。

useReducer hook 返回状态和一个 dispatch 函数,我们可以使用它来触发状态的更改。它的工作方式与 Redux 类似,但更简单。(我仍然建议以后学习 Redux,因为它对更复杂的应用程序更有用)。

因为并非所有组件都需要访问状态和调度,所以我们将它们分成 2 个上下文。

我们的新context.js文件如下所示:

context.js

import React, { useContext, useReducer } from "react";

import { themeReducer } from "./reducer";

const { createContext } = require("react");

const initialState = {
  switched: 0,
  theme: "light",
};

const ThemeStateContext = createContext();
const ThemeDispatchContext = createContext();

export const useThemeState = () => useContext(ThemeStateContext);
export const useThemeDispatch = () => useContext(ThemeDispatchContext);

export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={dispatch}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

太棒了,接下来让我们创建themeReducer文件中调用的第一个 reducer reducer.js

Reducer.js

export const themeReducer = (state, { type }) => {
  switch (type) {
    case "TOGGLE_THEME":
      return {
        ...state,
        switched: state.switched + 1,
        theme: state.theme === "light" ? "dark" : "light",
      };
    default:
      throw new Error(`Unhandled action type: ${type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

当带有标签“TOGGLE_THEME”的操作进入时,上述函数会更新状态。如果操作未知,则会引发错误。

我们还将在 context.js 文件中更新初始状态和主题:

context.js

import React, { useContext, useReducer } from "react";

import { createMuiTheme } from "@material-ui/core";
import { themeReducer } from "./reducer";

const { createContext } = require("react");

export const light = {
  palette: {
    type: "light",
  },
};

export const dark = {
  palette: {
    type: "dark",
  },
};

export const lightTheme = createMuiTheme(light);
export const darkTheme = createMuiTheme(dark);

const initialState = {
  switched: 0,
  theme: lightTheme,
};

const ThemeStateContext = createContext();
const ThemeDispatchContext = createContext();

export const useThemeState = () => useContext(ThemeStateContext);
export const useThemeDispatch = () => useContext(ThemeDispatchContext);

export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  return (
    <ThemeStateContext.Provider value={theme}>
      <ThemeDispatchContext.Provider value={dispatch}>
        {children}
      </ThemeDispatchContext.Provider>
    </ThemeStateContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

现在我们可以在应用程序的任何地方使用和:switched非常酷!themeconst { theme } = useThemeState()

创建主题切换

在 users.js 中我们现在可以使用我们的调度功能:

用户.js

....
const { theme } = useThemeState();
const dispatch = useThemeDispatch();
...
<Grid item onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
              {theme === "light" ? <Brightness7Icon /> : <Brightness4Icon />}
            </Grid>
Enter fullscreen mode Exit fullscreen mode

我们的主题切换功能正常,太棒了!

从 API 加载用户

让我们在上下文文件夹中创建一个新文件夹并调用它,users并添加与其中相同的文件,theme但现在也添加actions.js到其中。

我们将重复主题上下文的代码,只不过actions.js这次我们添加了 API 获取,并根据结果更新状态。我们的 Reducer 只需要直接更新状态,其他操作我们将像 Redux 一样分开执行。

actions.js

export const getUsers = async (dispatch) => {
  dispatch({ type: "REQUEST_USERS" });
  try {
    // Fetch server
    const response = await fetch(`https://jsonplaceholder.typicode.com/users`);

    if (!response.ok) {
      throw Error(response.statusText);
    }

    let data = await response.json();

    // Received users from server
    if (data.length) {
      dispatch({ type: "USERS_SUCCESS", payload: data });
      return data;
    }

    // No match found on server
    dispatch({
      type: "USERS_FAIL",
      error: { message: "Could not fetch users" },
    });

    return null;
  } catch (error) {
    dispatch({ type: "USERS_FAIL", error });
  }
};
Enter fullscreen mode Exit fullscreen mode

当上述函数被调用时,它将从 API 端点获取用户数据。“REQUEST_USERS”会将我们的状态设置为loading: true。如果返回了用户,我们将在 Reducer 中用它们更新状态;如果没有,我们也会更新状态错误对象:

Reducer.js

export const usersReducer = (state, { type, payload, error }) => {
  switch (type) {
    case "REQUEST_USERS":
      return {
        ...state,
        loading: true,
      };
    case "USERS_SUCCESS":
      return {
        ...state,
        loading: false,
        users: payload,
      };
    case "USERS_FAIL":
      return {
        ...state,
        loading: false,
        error,
      };
    default:
      throw new Error(`Unhandled action type: ${type}`);
  }
};
Enter fullscreen mode Exit fullscreen mode

现在,你需要将用户上下文包装到应用程序中。你可以按照我们之前对主题所做的操作来操作。

当用户点击“LOAD_USERS”按钮时,通过分派正确的操作来获取用户:

用户.js

...
  const dispatchUsers = useUsersDispatch();
  const _getUsers = () => getUsers(dispatchUsers);
...
<Button onClick={_getUsers} variant="contained" color="primary">
                Load users
              </Button>
...
Enter fullscreen mode Exit fullscreen mode

现在我们可以获取用户并将其保存在状态中,让我们在应用程序中显示它们:

用户列表.js

import { Grid } from "@material-ui/core";
import React from "react";
import User from "./User";
import { useUsersState } from "../context/users";

export default function UserList() {
  const { users, loading, error } = useUsersState();

  if (loading) {
    return "Loading...";
  }

  if (error) {
    return "Error...";
  }

  return (
    <Grid container spacing={2}>
      {users?.map((user, i) => (
        <Grid key={i} item xs={12} sm={6}>
          <User user={user} />
        </Grid>
      ))}
    </Grid>
  );
}
Enter fullscreen mode Exit fullscreen mode

当然,您可以添加一些很棒的加载微调器或显示更好的错误,但希望您能看到在需要的地方加载应用程序状态并相应地更新 UI 是多么容易。

我想邀请您添加删除功能!只需在删除按钮上添加一个调度函数,并根据用户的 ID 在 Reducer 中将其删除即可。

以下是代码:

用户.js

...
const dispatch = useUsersDispatch();
...
 <Button
          onClick={() => dispatch({ type: "DELETE_USER", payload: user.id })}
          size="small"
          variant="contained"
          color="secondary"
        >
          Delete
        </Button>
Enter fullscreen mode Exit fullscreen mode

Reducer.js

case "DELETE_USER":
      return {
        ...state,
        users: state.users.filter((user) => user.id !== payload),
      };
Enter fullscreen mode Exit fullscreen mode

坚持我们的状态

我们可以改进应用的最后一件事是,当用户关闭窗口时,保持应用状态。这可以通过将状态存储在用户本地存储中来实现,这称为持久化状态。

首先,每当我们的状态在 context.js 文件中发生变化时,我们都会将状态添加到本地存储中:

context.js

export const ThemeProvider = ({ children }) => {
  const [theme, dispatch] = useReducer(themeReducer, initialState);

  // Persist state on each update
  useEffect(() => {
    localStorage.setItem("theme", JSON.stringify(theme));
  }, [theme]);

  return ( ...
Enter fullscreen mode Exit fullscreen mode

然后,我们将改变初始状态以在本地存储中存储的状态可用时获取该状态,否则使用我们已经声明的初始状态。

我们将向 reducer 中传入一个初始化函数,而不是初始状态:

Reducer.js

...
const initialState = {
  loading: false,
  error: null,
  users: [],
};

const initializer = localStorage.getItem("users")
  ? JSON.parse(localStorage.getItem("users"))
  : initialState;
...
const [state, dispatch] = useReducer(usersReducer, initializer);
Enter fullscreen mode Exit fullscreen mode

我们将针对这两种情况都这样做。

您应该会在浏览器的本地存储中看到您的应用状态,太棒了!🔥

点击此处查看应用的实际运行情况。
点击 hero 查看 GitHub 仓库。

这些技术有如此多的可能性,我希望本教程能够以任何方式帮助您!

文章来源:https://dev.to/sanderdebr/flawless-react-state-management-usereducer-and-context-api-1a7g
PREV
如何在 React 项目中使用 Shadcn UI
NEXT
使用 Hooks 在 React 中创建 CRUD 应用程序 1. 设置项目 2. 添加用户表 3. 添加用户 4. 删除用户 5. 更新用户 6. 使用 Effect Hook 7. 额外奖励:从 API 获取用户