使用 React Context 编写简洁代码
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
假期里我有点空闲时间,于是决定仔细阅读一下鲍勃大叔的《代码整洁之道》,看看代码整洁架构的哪些原则(如果有的话)可以应用到我正在开发的一些 React 项目中。
这本书开篇不久,鲍勃大叔就开始讨论一个函数可以接受的参数总数超过一定限度后,代码就会显得不够“简洁”。他的目的是确保我们开发者编写的函数易于阅读和使用。因此,他的论点是,如果一个函数需要三个或更多参数,就会增加函数的复杂性,降低开发者阅读和理解函数用途的速度,同时也会增加错误使用的风险(尤其是在使用原生 JavaScript 或非 TypeScript 的项目中)。
这让我开始思考我是如何在 React 应用程序中处理用户警报的,以及或许在鲍勃大叔的帮助下,我可以如何稍微清理一下我的代码。
原始方法
这就是我们正在开发的项目:一个简单的 React 应用,其中包含一个 AlertContext 组件,该组件包裹了整个应用。这个 AlertContext 组件将控制生成的提示框的状态,并在屏幕底部渲染一个类似 Snackbar/Toast 的提示框。
由于此组件使用了 React Context,因此 AlertContext 提供程序中的每个子组件都可以使用该警报上下文,并根据需要为用户生成成功、警告或错误警报。为了简化示例,我只使用了三个按钮,每个按钮都位于各自的组件中。每个按钮都会生成不同类型的警报。
以下是原始 AlertContext 组件的截图。
// AlertContext.tsx
import React from "react";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
...
const AlertContext = React.createContext<IAlertContext>({
setAlertState: () => {}
});
const AlertProvider: React.FC = ({ children }) => {
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
const handleClose = (e: React.SyntheticEvent) => {
setAlertState((prev) => ({ ...prev, open: false }));
};
return (
<AlertContext.Provider value={{ setAlertState }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
};
export { AlertContext, AlertProvider };
在这里你可以看到,我使用 Material-UI Snackbar 和 MuiAlert 组件渲染了一个简单的 Alert。
// AlertContext.ts
return (
<AlertContext.Provider value={{ alertSuccess, alertError, alertWarning }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
然后由该对象控制,该alertState对象决定是否发出警报visible、severity警报的内容以及message应该显示的内容。
// AlertContext.ts
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
AlertContext 组件随后提供对该setAlertState方法的访问权限,允许任何使用 AlertContext 的子组件显示成功、警告和错误类型的警报消息。例如,这里有一个包含三个按钮的组件。点击每个按钮都会生成不同类型的警报,并显示不同的消息。
// AlertButtons.ts
import React from "react";
import { Button } from "@material-ui/core";
import { AlertContext } from "./AlertContext";
const AlertButtons = () => {
const { setAlertState } = React.useContext(AlertContext);
const handleSuccessClick = () => {
setAlertState({
open: true,
severity: "success",
message: "Successfull alert!"
});
};
const handleWarningClick = () => {
setAlertState({
open: true,
severity: "warning",
message: "Warning alert!"
});
};
const handleErrorClick = () => {
setAlertState({
open: true,
severity: "error",
message: "Error alert!"
});
};
return (
<div>
<Button variant="contained" onClick={handleSuccessClick}>
Success Button
</Button>
<Button variant="contained" onClick={handleWarningClick}>
Warning Button
</Button>
<Button variant="contained" onClick={handleErrorClick}>
Error Button
</Button>
</div>
);
};
export default AlertButtons;
要显示警报,我们必须首先从上下文提供程序中访问 setAlertState 方法。
// AlertButtons.tsx
const { setAlertState } = React.useContext(AlertContext);
现在我们可以在每个按钮的 onClick 函数中,或者在我们创建的任何其他函数中使用此方法。例如,每当用户点击“成功”按钮时,我们都会生成一个成功提示框,内容为“成功提示!”
// AlertButtons.tsx
const handleSuccessClick = () => {
setAlertState({
open: true,
severity: "success",
message: "Successfull alert!"
});
};
更清洁的方法
说实话,最初的方法可能没什么大问题。从技术上讲,`setAlertState` 方法只需要一个参数……只不过它恰好是一个包含三个不同属性的对象。仔细观察你会发现,其中一个属性“open”实际上并没有在每次调用它来显示新的提示状态时发生变化。如果只有我一个人在做这个项目,而且我每次都明白如何调用这个方法,那么这种方法可能完全没问题。但是如果我和其他开发者合作呢?`setAlertState(params: {...})` 方法在其他人看来是否清晰易懂呢?
因此,我尝试了一种更简洁的方法,即改变从 AlertContext 组件设置新警报的方式。我不再让每个子组件直接访问上下文的 setAlertState 函数,而是为每种生成的警报类型提供 3 个单独的方法。
// AlertContext.tsx
type IAlertContext = {
alertSuccess: (message: string) => void,
alertError: (message: string) => void,
alertWarning: (message: string) => void,
};
这些方法仅接受一个参数——消息,并且完全无需记住将警报状态设置为“打开”以及使用正确的警报严重性类型。您可以在下方看到我们创建了三个相应的方法:`get_set_alert_state`、`get_set_alert_state`和 `get_set_alert_state` 。每个方法都接受一个简单的消息作为输入,并且每个函数内部都会调用alertSuccess()相应的 `get_set_alert_state` 并传入相应的打开状态和严重性类型。alertWarning()alertError()setAlertState
// AlertContext.tsx
import React from "react";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
type IAlertState = {
open: boolean,
severity: "success" | "warning" | "error",
message: string,
};
type IAlertContext = {
alertSuccess: (message: string) => void,
alertError: (message: string) => void,
alertWarning: (message: string) => void,
};
const AlertContext = React.createContext<IAlertContext>({
alertSuccess: () => {},
alertError: () => {},
alertWarning: () => {}
});
const AlertProvider: React.FC = ({ children }) => {
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
const handleClose = (e: React.SyntheticEvent) => {
setAlertState((prev) => ({ ...prev, open: false }));
};
const alertSuccess = (message: string) => {
setAlertState({
open: true,
severity: "success",
message: message
});
};
const alertError = (message: string) => {
setAlertState({
open: true,
severity: "error",
message: message
});
};
const alertWarning = (message: string) => {
setAlertState({
open: true,
severity: "warning",
message: message
});
};
return (
<AlertContext.Provider value={{ alertSuccess, alertError, alertWarning }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
};
export { AlertContext, AlertProvider };
现在回到按钮组件内部,我们不再需要访问原来的 setAlertState 方法。取而代之的是,我们可以访问新的alertSuccess()、alertWarning()和alertError()函数。
const { alertSuccess, alertError, alertWarning } = React.useContext(
AlertContext
);
然后更新每个相应的 onClick 处理程序,以调用新导入的函数。
const handleSuccessClick = () => {
alertSuccess("Successfull alert!");
};
const handleWarningClick = () => {
alertWarning("Warning alert!");
};
const handleErrorClick = () => {
alertError("Error alert!");
};
值得吗?
对我来说,第二种方法看起来更简洁,而且我以后很可能会继续使用这种方法。使用第二种方法,我可以轻松地扩展 AlertContext,使其包含比我目前实现的更多严重类型,而不会影响任何子组件中的实现。对于任何偶然发现这段代码的开发者来说,第二种方法肯定更容易理解类似 `setTimeout` 这样的方法的用途和用法,alertSuccess(message: string)而不是像 `setTimeout` 那样的方法setAlertState(params: {...})。