React 设计模式(译)
React 开发人员可以通过使用设计模式来节省时间和精力,这些模式提供了一种使用经过测试和信任的解决方案解决问题的快速方法。它们使内聚模块具有较低的耦合性,这反过来又帮助 React 开发人员创建可维护、可扩展和高效的应用程序。在本文中,我们将探讨 React 设计模式,并研究它们如何改进 React 应用程序的开发。
容器和表示模式是一种模式,旨在将表示逻辑与反应代码中的业务逻辑分离,从而使其模块化、可测试,并遵循关注分离原则。 大多数情况下,在 react 应用程序中,我们需要从后端/存储中获取数据或计算逻辑,并在 react 组件上表示该计算的结果。在这些情况下,容器和表示模式大放异彩,因为它可用于将组件分为两个,即:
1//Container.tsx
2import React, { useEffect } from "react";
3import CharacterList from "./CharacterList";
4
5const StarWarsCharactersContainer: React.FC = () => {
6 const [characters, setCharacters] = useState<Character>([]);
7 const [isLoading, setIsLoading] = useState<boolean>(false);
8 const [error, setError] = useState<boolean>(false);
9
10 const getCharacters = async () => {
11 setIsLoading(true);
12 try {
13 const response = await fetch(
14 "https://akabab.github.io/starwars-api/api/all.json",
15 );
16 const data = await response.json();
17 setIsLoading(false);
18 if (!data) return;
19 setCharacters(data);
20 } catch (err) {
21 setError(true);
22 } finally {
23 setIsLoading(true);
24 }
25 };
26
27 useEffect(() => {
28 getCharacters();
29 }, []);
30
31 return (
32 <CharacterList loading={loading} error={error} characters={characters} />
33 );
34};
35
36export default StarWarsCharactersContainer;
37
1//the component is responsible for displaying the characters
2import React from "react";
3import { Character } from "./types";
4
5interface CharacterListProps {
6 loading: boolean;
7 error: boolean;
8 users: Character[];
9}
10
11const CharacterList: React.FC<CharacterListProps> = ({
12 loading,
13 error,
14 characters,
15}) => {
16 if (loading && !error) return <div>Loading...</div>;
17 if (!loading && error)
18 return <div>error occured.unable to load characters</div>;
19 if (!characters) return null;
20
21 return (
22 <ul>
23 {characters.map((user) => (
24 <li key={user.id}>{user.name}</li>
25 ))}
26 </ul>
27 );
28};
29
30export default CharacterList;
31
钩子是 React 16.8 中首次亮相的全新功能。从那时起,他们在开发 react 应用程序方面发挥了至关重要的作用。钩子是基本函数,用于授予功能组件对状态和生命周期方法的访问权限(以前仅供类组件使用)。另一方面,钩子可以专门设计来满足组件要求并具有其他用例。 现在,我们可以隔离所有有状态逻辑(一种需要反应式状态变量的逻辑),并使用自定义钩子在组件中编写或使用它。因此,代码更加模块化和可测试,因为钩子松散地绑定在组件上,因此可以单独测试。 下面显示了带有钩子的组件组合示例:
1// creating a custom hook that fetches star wars characters
2export const useFetchStarWarsCharacters = () => {
3 const [characters, setCharacters] = useState<Character>([]);
4 const [isLoading, setIsLoading] = useState(false);
5 const [error, setError] = useState(false);
6 const controller = new AbortController();
7
8 const getCharacters = async () => {
9 setIsLoading(true);
10 try {
11 const response = await fetch(
12 "https://akabab.github.io/starwars-api/api/all.json",
13 {
14 method: "GET",
15 credentials: "include",
16 mode: "cors",
17 headers: {
18 "Content-Type": "application/json",
19 "Access-Control-Allow-Origin": "*",
20 },
21 signal: controller.signal,
22 },
23 );
24 const data = await response.json();
25 setIsLoading(false);
26 if (!data) return;
27 setCharacters(data);
28 } catch (err) {
29 setError(true);
30 } finally {
31 setIsLoading(true);
32 }
33 };
34
35 useEffect(() => {
36 getCharacters();
37 return () => {
38 controller.abort();
39 };
40 }, []);
41
42 return [characters, isLoading, error];
43};
44
创建自定义钩子后,我们会将其导入到我们的 StarWarsCharactersContainer 组件中并使用它:
1// importing the custom hook to a component and fetch the characters
2
3import React from "react";
4import { Character } from "./types";
5import { useFetchStarWarsCharacters } from "./useFetchStarWarsCharacters";
6
7const StarWarsCharactersContainer: React.FC = () => {
8 const [characters, isLoading, error] = useFetchStarWarsCharacters();
9
10 return (
11 <CharacterList loading={loading} error={error} characters={characters} />
12 );
13};
14
15export default StarWarsCharactersContainer;
16
大多数情况下,处理组件中的许多状态会导致许多未分组状态的问题,这可能很麻烦且难以处理。在这种情况下,Reducers 模式可能是一个有用的选择。我们可以使用 reducer 将状态分类为某些操作,这些操作在执行时可以更改分组状态。 此模式允许使用它的开发人员控制组件和/或钩子的状态管理,让他们在发送事件时管理状态更改。 使用 Reducer 模式的示例如下所示:
1import {React, i useReducer } from 'react';
2const initstate = {
3 loggedIn: false,user: null,
4 token: nuli
5}
6function authReducer(state,action) {
7 switch (action.type) {
8 case 'login':
9 return {
10 loggedIn: true,
11 user: action. payload.user,token: action. payload.token
12 }
13 case 'logout':
14 return initState;
15 default:
16 break;
17 }
18}
19const AuthComponent =()=> {
20 const [state, dispatch] = useReducer(authReducer, initstate);
21 const logIn =()=>{
22 dispatch({type:'login',payload:{
23 user:{name:'John Doe'},
24 token:'token'
25 }});
26 }
27 const logout = ()=> {
28 dispatch({ type: 'logout' });
29 }
30return (
31 <div>
32 { state.loggedIn ? (
33 <div>
34 <p> Welcome { state.user.name }</ p><button onclick={logout}></ button>
35 </div>
36 ):(
37 <form onSubmit={logIn}>
38 <input type="text"/>
39 <input type="password"/>
40 <button type="submit"></button>
41 </form>
42 )
43 }
44 </div>
45)
46}
47
在上面的代码中,该组件调度了两个操作:
提供程序模式对于数据管理非常有用,因为它利用上下文 API 通过应用程序的组件树传递数据。这种模式是 Props drilling 的有效解决方案,Props drilling 一直是 React 开发中常见的问题。 为了实现提供程序模式,我们将首先创建一个提供程序组件。提供程序是 Context 对象提供给我们的高阶组件。我们可以利用 React 提供的 createContext 方法构造一个 Context 对象。
1export const ThemeContext = React.createContext(null);
2
3export function ThemeProvider({ children }) {
4 const [theme, setTheme] = React.useState("light");
5
6 return (
7 <ThemeContext.Provider value={{ theme, setTheme }}>
8 {children}
9 </ThemeContext.Provider>
10 );
11}
12
创建提供程序后,我们将使用创建的提供程序组件将依赖于上下文 API 中数据的组件封闭起来。 为了从上下文 API 获取数据,我们调用 useContext 钩子,它接受上下文作为参数(在本例中为 ThemeContext)。
1import { useContext } from "react";
2import { ThemeProvider, ThemeContext } from "../context";
3
4const HeaderSection = () => {
5 <ThemeProvider>
6 <TopNav />
7 </ThemeProvider>;
8};
9
10const TopNav = () => {
11 const { theme, setTheme } = useContext(ThemeContext);
12
13 return (
14 <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
15 ...
16 </div>
17 );
18};
19
高阶组件将组件作为参数,并返回注入了附加数据或功能的增压组件。React 中 HOC 的可能性是由于 React 对组合的偏好而不是继承。 高阶组件 (HOC) 模式提供了一种增加或修改组件功能的机制,从而促进了组件重用和代码共享。 HOC 模式的示例如下所示:
1import React from 'react'
2
3const higherOrderComponent = Component => {
4 return class HOC extends React.Component {
5 state = {
6 name: 'John Doe'
7 }
8
9 render() {
10 return <Component name={this.state.name {...this.props}} />
11 }
12 }}
13
14
15const AvatarComponent = (props) => {
16 return (
17 <div className="flex items-center justify-between">
18 <div className="rounded-full bg-red p-4">
19 {props.name}
20 </div>
21 <div>
22 <p>I am a {props.description}.</p>
23 </div>
24 </div>
25 )
26}
27
28
29const SampleHOC = higherOrderComponent(AvatarComponent);
30
31
32const App = () => {
33 return (
34 <div>
35 <SampleHOC description="Frontend Engineer" />
36 </div>
37 )
38}
39
40export default App;
41
在上面的代码中,由 higherOrderComponent 提供 props,它将在内部使用。
复合组件模式是一种 React 设计模式,用于管理由子组件组成的父组件。 这种模式背后的原理是将父组件分解为更小的组件,然后使用道具、上下文或其他 React 数据管理技术来管理这些较小组件之间的交互。 当需要创建由较小组件组成的可重用的多功能组件时,此模式会派上用场。它使开发人员能够创建复杂的 UI 组件,这些组件可以轻松自定义和扩展,同时保持清晰简单的代码结构。 复合组件模式的用例示例如下所示:
1import React, { createContext, useState } from "react";
2
3const ToggleContext = createContext();
4
5function Toggle({ children }) {
6 const [on, setOn] = useState(false);
7 const toggle = () => setOn(!on);
8
9 return (
10 <ToggleContext.Provider value={{ on, toggle }}>
11 {children}
12 </ToggleContext.Provider>
13 );
14}
15
16Toggle.On = function ToggleOn({ children }) {
17 const { on } = useContext(ToggleContext);
18 return on ? children : null;
19};
20
21Toggle.Off = function ToggleOff({ children }) {
22 const { on } = useContext(ToggleContext);
23 return on ? null : children;
24};
25
26Toggle.Button = function ToggleButton(props) {
27 const { on, toggle } = useContext(ToggleContext);
28 return <button onClick={toggle} {...props} />;
29};
30
31function App() {
32 return (
33 <Toggle>
34 <Toggle.On>The button is on</Toggle.On>
35 <Toggle.Off>The button is off</Toggle.Off>
36 <Toggle.Button>Toggle</Toggle.Button>
37 </Toggle>
38 );
39}
40
这需要从几个相关的 prop 中创建一个对象,并将其作为单个 prop 传递给组件。 这种模式允许我们清理代码并简化道具的管理,这在我们想要将大量相关属性传递给组件时特别有用。
1import React from "react";
2
3function P(props) {
4 const { color, size, children, ...rest } = props;
5 return (
6 <p style={{ color, fontSize: size }} {...rest}>
7 {children}
8 </p>
9 );
10}
11
12function App() {
13 const paragraphProps = {
14 color: "red",
15 size: "20px",
16 lineHeight: "22px",
17 };
18 return <P {...paragraphProps}>This is a P</P>;
19}
20
受控输入模式可用于处理输入字段。此模式涉及使用事件处理程序在输入字段的值发生更改时更新组件状态,以及将输入字段的当前值存储在组件状态中。 由于 React 控制组件的状态和行为,因此这种模式使代码比不受控制的输入模式更具可预测性和可读性,后者不使用组件的状态,而是直接通过 DOM(文档对象模型)来控制它。 受控输入模式的用例示例如下所示:
1import React, { useState } from "react";
2
3function ControlledInput() {
4 const [inputValue, setInputValue] = useState("");
5
6 const handleChange = (event) => {
7 setInputValue(event.target.value);
8 };
9
10 return <input type="text" value={inputValue} onChange={handleChange} />;
11}
12
称为 ForwardRef 的高阶组件将另一个组件作为输入,并输出一个传递原始组件的 ref 的新组件。通过这样做,子组件的 ref(可用于检索底层 DOM 节点或组件实例)可供父组件访问。 创建与第三方库或应用程序中的其他自定义组件交互的自定义组件时,在工作流中包含 ForwardRef 模式非常有帮助。通过授予对库的 DOM 节点或其他组件的 DOM 实例的访问权限,它有助于将此类组件的控制权转移给您。 下面显示了 forwardRef 模式的用例示例:
1import React from "react";
2
3const CustomInput = React.forwardRef((props, ref) => (
4 <input type="text" {...props} ref={ref} />
5));
6
7const ParentComponent = () => {
8 const inputRef = useRef(null);
9
10 useEffect(() => {
11 inputRef.current.focus();
12 }, []);
13
14 return <CustomInput ref={inputRef} />;
15};
16
在上面的代码中,我们 <CustomInput/>
<ParentComponent/>
使用 forwardRefs .
我们在本文中讨论了 React 设计模式,包括高阶组件、容器表示组件模式、复合组件、受控组件等等。通过将这些设计模式和最佳实践整合到您的 React 项目中,您可以提高代码质量、促进团队协作,并使您的应用程序更具可扩展性、灵活性和可维护性。