完美的 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>
</>
);
}
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;
用户.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>
);
}
此外,我已经为我们稍后将进行的主题切换添加了默认图标。
添加用户列表
现在让我们添加保存用户信息的卡片。
用户列表.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>
);
}
用户.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>
);
}
我们目前只使用了一些硬编码的用户数据和一个包含 3 个元素的数组来显示用户信息。在下一节中,我们将通过 API 加载用户信息并将其存储在应用状态中。
在此之前,让我们首先使用 Context API 创建主题切换。
💡 添加 Context API
创建一个名为 的新文件夹context
,并在此处添加一个名为 的文件夹theme
。在此文件夹中,我们将创建以下 3 个文件:context.js
、index.js
和reducer.js
。
我将逐步解释每个文件。
context.js
我们将使用 React 的 Context API 来包装我们的应用程序,并提供一些我们想要提供的值,在本例中是主题设置。
首先,我们将创建一个新的上下文”
const { createContext } = require("react");
const ThemeContext = createContext();`
然后我们将设置一个包装函数,为整个应用程序提供主题:
<ThemeContext.Provider theme={currentTheme} setTheme={setTheme}>
{children}
</ThemeContext.Provider>
为了使其与 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>
);
};
接下来我们将使用index.js
轻松将上下文文件导入其他文件:
index.js
import { useTheme, ThemeProvider } from "./context";
export { useTheme, ThemeProvider };
我们将App.js
使用提供程序将我们的应用程序包装在里面:
App.js
...
function App() {
return (
<ThemeProvider>
...
</ThemeProvider>
);
}
...
我们将更新_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>
);
}
现在,我们可以通过useTheme()
钩子在应用的任何地方使用该主题。例如,Users.js
我们可以添加以下内容,根据主题设置显示太阳或月亮:
const theme = useTheme();
{theme.palette.type === "light" ?
<Brightness7Icon /> : <Brightness4Icon />}
这非常有用,我们为应用添加了一个全局状态!但是如果我们想更新该状态怎么办?这时 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>
);
};
太棒了,接下来让我们创建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}`);
}
};
当带有标签“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>
);
};
现在我们可以在应用程序的任何地方使用和:switched
。非常酷!theme
const { theme } = useThemeState()
创建主题切换
在 users.js 中我们现在可以使用我们的调度功能:
用户.js
....
const { theme } = useThemeState();
const dispatch = useThemeDispatch();
...
<Grid item onClick={() => dispatch({ type: "TOGGLE_THEME" })}>
{theme === "light" ? <Brightness7Icon /> : <Brightness4Icon />}
</Grid>
我们的主题切换功能正常,太棒了!
从 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 });
}
};
当上述函数被调用时,它将从 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}`);
}
};
现在,你需要将用户上下文包装到应用程序中。你可以按照我们之前对主题所做的操作来操作。
当用户点击“LOAD_USERS”按钮时,通过分派正确的操作来获取用户:
用户.js
...
const dispatchUsers = useUsersDispatch();
const _getUsers = () => getUsers(dispatchUsers);
...
<Button onClick={_getUsers} variant="contained" color="primary">
Load users
</Button>
...
现在我们可以获取用户并将其保存在状态中,让我们在应用程序中显示它们:
用户列表.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>
);
}
当然,您可以添加一些很棒的加载微调器或显示更好的错误,但希望您能看到在需要的地方加载应用程序状态并相应地更新 UI 是多么容易。
我想邀请您添加删除功能!只需在删除按钮上添加一个调度函数,并根据用户的 ID 在 Reducer 中将其删除即可。
以下是代码:
用户.js
...
const dispatch = useUsersDispatch();
...
<Button
onClick={() => dispatch({ type: "DELETE_USER", payload: user.id })}
size="small"
variant="contained"
color="secondary"
>
Delete
</Button>
Reducer.js
case "DELETE_USER":
return {
...state,
users: state.users.filter((user) => user.id !== payload),
};
坚持我们的状态
我们可以改进应用的最后一件事是,当用户关闭窗口时,保持应用状态。这可以通过将状态存储在用户本地存储中来实现,这称为持久化状态。
首先,每当我们的状态在 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 ( ...
然后,我们将改变初始状态以在本地存储中存储的状态可用时获取该状态,否则使用我们已经声明的初始状态。
我们将向 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);
我们将针对这两种情况都这样做。
您应该会在浏览器的本地存储中看到您的应用状态,太棒了!🔥
点击此处查看应用的实际运行情况。
点击 hero 查看 GitHub 仓库。
这些技术有如此多的可能性,我希望本教程能够以任何方式帮助您!
文章来源:https://dev.to/sanderdebr/flawless-react-state-management-usereducer-and-context-api-1a7g