React 设计模式总结

2025-05-26

React 设计模式

结论


精炼回购

作者:彼得·奥萨

介绍:

React 开发者可以通过使用设计模式来节省时间和精力,这些模式提供了一种使用经过测试和信任的解决方案快速解决问题的方法。它们能够实现低耦合、高内聚的模块,从而帮助 React 开发者创建可维护、可扩展且高效的应用程序。在本文中,我们将探索 React 设计模式,并分析它们如何改进 React 应用程序的开发。

容器和演示模式

容器和展示模式旨在将 React 代码中的展示逻辑与业务逻辑分离,从而使代码模块化、可测试,并遵循关注点分离原则。
在 React 应用中,我们经常需要从后端/存储获取数据,或者计算逻辑并在 React 组件上呈现计算结果。在这些情况下,容器和展示模式就显得尤为重要,因为它可以将组件分为两类:

  • 容器组件,作为负责数据获取或计算的组件。
  • 表示组件,其作用是在 UI(用户界面)上呈现获取的数据或计算值。

容器和表示模式的示例如下所示:

import React, { useEffect } from 'react';
import CharacterList from './CharacterList';

const StarWarsCharactersContainer:React.FC = () => {
    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState<boolean>(false);
    const [error, setError] = useState<boolean>(false);

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch("https://akabab.github.io/starwars-api/api/all.json");
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
    }, []);

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;
Enter fullscreen mode Exit fullscreen mode
// the component is responsible for displaying the characters

import React from 'react';
import { Character } from './types';

interface CharacterListProps {
    loading: boolean;
    error: boolean;
    users: Character[];
}

const CharacterList: React.FC<CharacterListProps> = ({ loading, error, characters }) => {

    if (loading && !error) return <div>Loading...</div>;
    if (!loading && error) return <div>error occured.unable to load characters</div>;
    if (!characters) return null;

    return (
        <ul>
            {characters.map((user) => (
                <li key={user.id}>{user.name}</li>
            ))}
        </ul>
    );
};

export default CharacterList;
Enter fullscreen mode Exit fullscreen mode

使用 Hooks 进行组件组合

Hooks 是 React 16.8 中首次推出的一项全新功能。自此之后,它们在 React 应用开发中扮演着至关重要的角色。Hooks 是一些基础函数,它允许函数式组件访问状态和生命周期方法(这些方法以前只能由类组件访问)。而 Hooks 则可以专门设计,以满足组件的特定需求,并拥有更多用例。

现在,我们可以隔离所有状态逻辑(一种需要响应式状态变量的逻辑),并使用自定义钩子在组件中组合或使用它。这样一来,代码就更加模块化和易于测试了,因为钩子与组件的绑定比较松散,因此可以单独进行测试。

带有钩子的组件组合示例如下所示:

// creating a custom hook that fetches star wars characters

export const useFetchStarWarsCharacters = () => {

    const [characters, setCharacters] = useState<Character>([])
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError] = useState(false);
    const controller = new AbortController()

    const getCharacters = async () => {
        setIsLoading(true);
        try {
            const response = await fetch(
                "https://akabab.github.io/starwars-api/api/all.json", 
                {
                    method: "GET", 
                    credentials: "include",
                    mode: "cors",
                    headers: {
                        'Content-Type': 'application/json',
                        'Access-Control-Allow-Origin': '*'
                    },
                    signal: controller.signal
                }
            );
            const data = await response.json();
            setIsLoading(false);
            if (!data) return;
            setCharacters(data);
        } catch(err) {
            setError(true);
        } finally {
            setIsLoading(true);
        }
    };

    useEffect(() => {
        getCharacters();
        return () => {
            controller.abort();
        }
    }, []);

    return [
        characters,
        isLoading,
        error
    ];
};
Enter fullscreen mode Exit fullscreen mode

创建自定义钩子后,我们将其导入到我们的StarWarsCharactersContainer组件中并使用它;

// importing the custom hook to a component and fetch the characters 

import React from 'react';
import { Character } from './types';
import { useFetchStarWarsCharacters } from './useFetchStarWarsCharacters';

const StarWarsCharactersContainer:React.FC = () => {

    const [ characters, isLoading, error ] = useFetchStarWarsCharacters();

    return <CharacterList loading={loading} error={error} characters={characters} />;
};

export default StarWarsCharactersContainer;

Enter fullscreen mode Exit fullscreen mode


使用 Reducers 进行状态管理

通常,处理组件中的大量状态会导致许多未分组状态的问题,这可能会带来繁琐且难以处理。在这种情况下,Reducer 模式可能是一个有用的选择。我们可以使用 Reducer 将状态分类为某些操作,这些操作在执行时可以更改分组状态。

这种模式允许使用它的开发人员控制组件和/或钩子的状态管理,让他们管理发送事件时的状态变化。

下面是使用 Reducer 模式的一个示例:

图片描述

在上面的代码中,组件调度了两个动作:

  • 登录”操作类型会触发状态变化,影响三个状态值,即登录用户令牌
  • 注销”操作只是将状态重置为其初始值。

使用提供商进行数据管理

提供程序模式对于数据管理非常有用,因为它利用 context API 在应用程序的组件树中传递数据。该模式是解决 prop 钻取问题的有效方案,而 prop 钻取一直是 React 开发中常见的问题。

为了实现提供程序模式,我们首先要创建一个提供程序组件。提供程序是 Context 对象提供给我们的高阶组件。我们可以利用 React 提供的 createContext 方法构造一个 Context 对象。

export const ThemeContext = React.createContext(null);

export function ThemeProvider({ children }) {
  const [theme, setTheme] = React.useState("light");

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

创建提供程序后,我们将使用创建的提供程序组件封装依赖于来自上下文 API 的数据的组件。

为了从上下文 API 获取数据,我们调用 useContext hook,它接受上下文作为参数(在本例中为ThemeContext)。

import { useContext } from 'react';
import { ThemeProvider, ThemeContext } from "../context";


const HeaderSection = () => {
  <ThemeProvider>
    <TopNav />
  </ThemeProvider>;
};


const TopNav = () => {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === "light" ? "#fff" : "#000 " }}>
      ...
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

使用 HOC(高阶组件)增强组件

高阶组件接受一个组件作为参数,并返回一个注入了额外数据或功能的增强组件。React 中之所以能够使用 HOC,是因为 React 更倾向于组合而非继承。

高阶组件 (HOC) 模式提供了一种增加或修改组件功能的机制,促进了组件重用和代码共享。

HOC 模式的一个例子如下所示:

import React from 'react'

const higherOrderComponent = Component => {
  return class HOC extends React.Component {
    state = {
      name: 'John Doe'
    }

    render() {
      return <Component name={this.state.name {...this.props} />
    }
 }


const AvatarComponent = (props) => {
  return (
    <div class="flex items-center justify-between">
      <div class="rounded-full bg-red p-4">
          {props.name}
      </div>
      <div>
          <p>I am a {props.description}.</p>
      </div>
    </div>
  )
}


const SampleHOC = higherOrderComponent(AvatarComponent);


const App = () => {
  return (
    <div>
      <SampleHOC description="Frontend Engineer" />
    </div>
  )
}

export default App;
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,由higherOrderComponent提供 props ,它将在内部利用它。

复合组件

复合组件模式是一种 React 设计模式,用于管理由子组件组成的父组件。

这种模式背后的原理是将父组件分解为更小的组件,然后使用 props、context 或其他反应数据管理技术来管理这些较小组件之间的交互。

当需要创建由较小组件组成的可重用、多功能组件时,此模式非常有用。它使开发人员能够创建易于定制和扩展的复杂 UI 组件,同时保持清晰简洁的代码结构。

复合组件模式的用例示例如下所示:

import React, { createContext, useState } from 'react';

const ToggleContext = createContext();

function Toggle({ children }) {
  const [on, setOn] = useState(false);
  const toggle = () => setOn(!on);

  return (
    <ToggleContext.Provider value={{ on, toggle }}>
      {children}
    </ToggleContext.Provider>
  );
}

Toggle.On = function ToggleOn({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? children : null;
}

Toggle.Off = function ToggleOff({ children }) {
  const { on } = useContext(ToggleContext);
  return on ? null : children;
}

Toggle.Button = function ToggleButton(props) {
  const { on, toggle } = useContext(ToggleContext);
  return <button onClick={toggle} {...props} />;
}

function App() {
  return (
    <Toggle>
      <Toggle.On>The button is on</Toggle.On>
      <Toggle.Off>The button is off</Toggle.Off>
      <Toggle.Button>Toggle</Toggle.Button>
    </Toggle>
  );
}
Enter fullscreen mode Exit fullscreen mode

道具组合

这需要从几个相关的道具中创建一个对象,并将其作为单个道具传递给组件。

这种模式使我们能够清理代码并使管理道具变得更简单,当我们想要将大量相关属性传递给组件时,它特别有用。

import React from 'react';

function P(props) {
  const { color, size, children, ...rest } = props;
  return (
    <p style={{ color, fontSize: size }} {...rest}>
      { children }
    </p>
  );
}

function App() {
  const paragraphProps = {
    color: "red",
    size: "20px",
    lineHeight: "22px"
  };
  return <P {...paragraphProps}>This is a P</P>;
}
Enter fullscreen mode Exit fullscreen mode

受控输入

受控输入模式可用于处理输入字段。此模式涉及使用事件处理程序在输入字段的值发生变化时更新组件状态,以及将输入字段的当前值存储在组件状态中。

由于 React 控制组件的状态和行为,因此这种模式使代码比不受控制的输入模式更可预测和可读,后者不使用组件的状态,而是直接通过 DOM(文档对象模型)进行控制。

受控输入模式的用例示例如下所示:

import React, { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (event) => {
    setInputValue(event.target.value);
  };

  return (
    <input type="text" value={inputValue} onChange={handleChange} />
  );
}
Enter fullscreen mode Exit fullscreen mode

使用 forwardRefs 管理自定义组件

一个名为 ForwardRef 的高阶组件接受另一个组件作为输入,并输出一个传递原始组件引用的新组件。这样,父组件就可以访问子组件的引用,该引用可用于检索底层 DOM 节点或组件实例。

在创建与第三方库或应用程序中的其他自定义组件交互的自定义​​组件时,在工作流程中包含 ForwardRef 模式非常有帮助。通过授予对库的 DOM 节点或其他组件的 DOM 实例的访问权限,它有助于将这些组件的控制权转移给您。

下面是 forwardRef 模式用例的示例:

import React from "react";

const CustomInput = React.forwardRef((props, ref) => (
  <input type="text" {...props} ref={ref} />
));

const ParentComponent = () => {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus();
  }, []);

  return <CustomInput ref={inputRef} />;
};
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我们使用<CustomInput/>从我们的组件触发了另一个组件的焦点<ParentComponent/>forwardRefs

结论

我们在本文中讨论了 React 设计模式,包括高阶组件、容器展示组件模式、复合组件、受控组件等等。将这些设计模式和最佳实践融入到你的 React 项目中,可以提升代码质量,促进团队协作,并使你的应用更具可扩展性、灵活性和可维护性。

文章来源:https://dev.to/refine/react-design-patterns-230o
PREV
使用 TypeScript 和 InversifyJS 在 Node.js 中实现 SOLID 和洋葱架构的先决条件
NEXT
最佳开源无头 CMS,值得为您的下一个应用程序尝试