mcpField
H
hooks-pattern

hooks-pattern

github.com/patternsdev开发2571.1k 次安装28 周前更新
代码仓库

关于 hooks-pattern

介绍 React Hooks 的开发技能,讲解如何在函数组件中管理状态和生命周期,并抽取可复用的状态逻辑。适合希望组织 React 组件逻辑、减少对类组件依赖的开发者。

主要功能

  1. 01用 useState 管理组件状态
  2. 02用 useEffect 处理副作用
  3. 03创建并复用自定义 Hook
  4. 04遵循 Hook 调用规则
  5. 05避免不必要的 useEffect

适用场景

  1. 01为函数组件添加状态行为
  2. 02在多个组件间复用状态逻辑
  3. 03将类组件改写为函数组件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill hooks-pattern

安装后在 Claude Code 中输入/hooks-pattern确认已加载。

详细介绍

Hooks 模式

目录

React 16.8 引入了一项名为 Hooks 的新功能。Hooks 让你无需使用 ES2015 类组件,就能使用 React 状态和生命周期方法。

Hooks 虽然不一定算是一种设计模式,但在应用设计中扮演着非常重要的角色。许多传统设计模式都可以用 Hooks 取代。

何时使用

  • 需要为函数组件添加状态或生命周期行为时,请使用 Hooks
  • 这有助于提取状态逻辑,并在多个组件间复用
  • 使用 Hooks 取代类组件,让代码更简洁、更易组合

使用说明

  • 在函数组件中,使用 useState 管理本地状态,使用 useEffect 处理副作用
  • 创建自定义 Hooks(名称以 use 为前缀),封装并共享可复用的逻辑
  • 遵循 Hooks 规则:只在顶层调用 Hooks,并且只在 React 函数中调用
  • 避免不必要的 useEffect——直接在组件主体中计算派生状态
  • 尽可能让 React Compiler 处理记忆化,而不是手动使用 useMemo/useCallback

详情

类组件

在 React 引入 Hooks 之前,我们必须使用类组件,才能为组件添加状态和生命周期方法。React 中一个典型的类组件可能如下所示:

class MyComponent extends React.Component {
  /* Adding state and binding custom methods */
  constructor() {
    super()
    this.state = { ... }

    this.customMethodOne = this.customMethodOne.bind(this)
    this.customMethodTwo = this.customMethodTwo.bind(this)
  }

  /* Lifecycle Methods */
  componentDidMount() { ...}
  componentWillUnmount() { ... }

  /* Custom methods */
  customMethodOne() { ... }
  customMethodTwo() { ... }

  render() { return { ... }}
}

类组件可以在构造函数中包含状态,使用 componentDidMount 和 componentWillUnmount 等生命周期方法,根据组件的生命周期执行副作用,也可以通过自定义方法为类添加额外逻辑。

虽然 React Hooks 引入后仍然可以使用类组件,但类组件也有一些缺点!下面来看看使用类组件时最常见的一些问题。

理解 ES2015 类

在 React Hooks 出现之前,类组件是唯一能够处理状态和生命周期方法的组件,因此我们经常不得不将函数组件重构为类组件,以添加额外功能。

在这个示例中,我们有一个简单的 div,它的作用是充当按钮。

function Button() {
  return <div className="btn">disabled</div>;
}

我们不希望它始终显示 disabled,而是希望用户点击按钮时将其改为 enabled,并在此时为按钮添加一些额外的 CSS 样式。

为此,我们需要为组件添加状态,以便判断状态是 enabled 还是 disabled。这意味着我们必须彻底重构函数组件,将其改为跟踪按钮状态的类组件。

export default class Button extends React.Component {
  constructor() {
    super();
    this.state = { enabled: false };
  }

  render() {
    const { enabled } = this.state;
    const btnText = enabled ? "enabled" : "disabled";

    return (
      <div
        className={`btn enabled-${enabled}`}
        onClick={() => this.setState({ enabled: !enabled })}
      >
        {btnText}
      </div>
    );
  }
}

在这个示例中,组件很小,重构起来并不麻烦。不过,实际项目中的组件可能包含更多代码行,因此重构组件会困难得多。

除了确保重构时不会意外改变任何行为外,你还需要理解 ES2015 类的工作方式。为什么必须对自定义方法使用 bind?constructor 有什么作用?this 关键字从何而来?如果不想意外改变数据流,正确地重构组件可能会很困难。

重构

在多个组件之间共享代码的常见方式,是使用高阶组件或 Render Props 模式。虽然这两种模式都有效,也是良好的实践,但如果要在后续阶段引入这些模式,就需要重构应用。

除了必须重构应用之外(组件越大,重构就越棘手),为了在更深层嵌套的组件之间共享代码而使用许多包装组件,还可能导致一种常被称为 包装地狱 的情况。打开开发者工具时,看到类似这样的结构并不少见:

<WrapperOne>
  <WrapperTwo>
    <WrapperThree>
      <WrapperFour>
        <WrapperFive>
          <Component>
            <h1>Finally in the component!</h1>
          </Component>
        </WrapperFive>
      </WrapperFour>
    </WrapperThree>
  </WrapperTwo>
</WrapperOne>

包装地狱 会让人难以理解数据如何在应用中流动,因此也更难弄清楚意外行为发生的原因。

复杂性

随着我们向类组件添加更多逻辑,组件的体积会迅速增大。组件内的逻辑可能会变得纠缠不清、缺乏结构,让开发者难以理解类组件中的特定逻辑用在何处。这会增加调试和优化性能的难度。

生命周期方法也会导致代码中出现大量重复。

即使组件很小,其中的逻辑也可能已经相当纠缠不清。有些部分专用于 counter 逻辑,另一些部分则专用于 width 逻辑。随着组件不断增大,在组件内组织逻辑、查找相关逻辑会变得越来越困难。

除了逻辑纠缠不清,我们还在生命周期方法中重复了一些逻辑。在 componentDidMount 和 componentWillUnmount 中,我们都根据窗口的 resize 事件自定义了应用的行为。

Hooks

很明显,类组件并不总是 React 的优秀特性。为了解决 React 开发者使用类组件时可能遇到的常见问题,React 引入了 React Hooks。React Hooks 是可用于管理组件状态和生命周期方法的函数。React Hooks 让我们能够:

  • 为函数组件添加状态
  • 无需使用 componentDidMount 和 componentWillUnmount 等生命周期方法,即可管理组件的生命周期
  • 在整个应用的多个组件之间复用相同的有状态逻辑

首先,来看看如何使用 React Hooks 为函数组件添加状态。

State Hook

React 提供了一个名为 useState 的 Hook,用于管理函数组件中的状态。

接下来看看如何使用 useState Hook,将类组件重构为函数组件。我们有一个名为 Input 的类组件,它只渲染一个输入框。每当用户在输入框中输入内容时,状态中的 input 值就会更新。

class Input extends React.Component {
  constructor() {
    super();
    this.state = { input: "" };

    this.handleInput = this.handleInput.bind(this);
  }

  handleInput(e) {
    this.setState({ input: e.target.value });
  }

  render() {
    <input onChange={handleInput} value={this.state.input} />;
  }
}

要使用 useState Hook,我们需要访问 React 提供的 useState 方法。useState 方法需要一个参数:状态的初始值,在本例中是空字符串。

我们可以从 useState 方法中解构出两个值:

  1. 状态的当前值。
  2. 用于更新状态的方法。
const [value, setValue] = React.useState(initialValue);

第一个值可以与类组件的 this.state.[value] 相对应。 第二个值可以与类组件的 this.setState 方法相对应。

由于我们处理的是输入框的值,因此将状态的当前值称为 input,将用于更新状态的方法称为 setInput。初始值应为空字符串。

const [input, setInput] = React.useState("");

现在,我们可以将 Input 类组件重构为有状态的函数组件。

function Input() {
  const [input, setInput] = React.useState("");

  return <input onChange={(e) => setInput(e.target.value)} value={input} />;
}

input 字段的值等于 input 状态的当前值,与类组件示例相同。当用户在输入框中输入内容时,input 状态的值会通过 setInput 方法相应更新。

Effect Hook

我们已经看到,可以使用 useState 组件在函数组件中处理状态,但类组件的另一项优势是可以为组件添加生命周期方法。

借助 useEffect Hook,我们可以“接入”组件的生命周期。useEffect Hook 实际上结合了 componentDidMount、componentDidUpdate 和 componentWillUnmount 生命周期方法。

componentDidMount() { ... }
useEffect(() => { ... }, [])

componentWillUnmount() { ... }
useEffect(() => { return () => { ... } }, [])

componentDidUpdate() { ... }
useEffect(() => { ... })

我们来使用 State Hook 部分中的输入示例。每当用户在输入框中输入内容时,我们还希望将该值记录到控制台。

我们需要使用一个 useEffect Hook 来“监听”input 值。为此,可以将 input 添加到 useEffect Hook 的依赖数组中。依赖数组是 useEffect Hook 接收的第二个参数。

useEffect(() => {
  console.log(`The user typed ${input}`);
}, [input]);

现在,每当用户输入一个值,该值都会记录到控制台。

自定义 Hooks

除了 React 提供的内置 Hooks(useState、useEffect、useReducer、useRef、useContext、useMemo、useImperativeHandle、useLayoutEffect、useDebugValue、useCallback)之外,我们还可以轻松创建自己的自定义 Hooks。

你可能已经注意到,所有 Hooks 都以 use 开头。Hooks 的名称以 use 开头很重要,这样 React 才能检查是否违反了 Hooks 规则。

假设我们想跟踪用户在输入时可能按下的某些按键。自定义 Hook 应能接收我们想要监听的按键作为参数。

function useKeyPress(targetKey) {
  const [keyPressed, setKeyPressed] = React.useState(false);

  function handleDown({ key }) {
    if (key === targetKey) {
      setKeyPressed(true);
    }
  }

  function handleUp({ key }) {
    if (key === targetKey) {
      setKeyPressed(false);
    }
  }

  React.useEffect(() => {
    window.addEventListener("keydown", handleDown);
    window.addEventListener("keyup", handleUp);

    return () => {
      window.removeEventListener("keydown", handleDown);
      window.removeEventListener("keyup", handleUp);
    };
  }, []);

  return keyPressed;
}

现在,我们不必将按键逻辑局限在 Input 组件中,而是可以在多个组件中重复使用 useKeyPress Hook,无需一遍又一遍地重写相同的逻辑。

Hooks 的另一项显著优势是,社区可以构建并分享 Hooks。以下网站列出了社区构建的、可在应用程序中直接使用的 Hooks:

使用 React Hooks 重写计数器和宽度示例后,我们可以将 App 函数的逻辑拆分为几个部分:

  • useCounter:一个自定义 Hook,返回 count 的当前值、increment 方法和 decrement 方法。
  • useWindowWidth:一个自定义 Hook,返回窗口的当前宽度。
  • App:一个有状态的函数组件,返回 Counter 和 Width 组件。

通过使用 React Hooks 而非类组件,我们能够将逻辑拆分为更小、可复用的部分,从而分离逻辑。

使用 React Hooks 让我们能够更清晰地将组件逻辑拆分为几个更小的部分。_复用_相同的有状态逻辑也变得容易得多;如果想让组件具有状态,我们不再需要将函数组件重写为类组件。不再需要深入掌握 ES2015 类,而且可复用的有状态逻辑还能提高组件的可测试性、灵活性和可读性。

Hooks 补充指南

与其他组件一样,如果想将 Hooks 添加到已编写的代码中,也会用到一些特殊函数。下面简要介绍一些常见的 Hook 函数:

useState

useState Hook 让开发者无需将函数组件转换为类组件,就能在其中更新和操作状态。这一 Hook 的优势之一是简单易用,且不需要像其他 React Hooks 那样复杂。

useEffect

useEffect Hook 用于在函数组件的主要生命周期事件中运行代码。函数组件的主体中不允许进行修改、订阅、设置计时器、记录日志以及执行其他副作用。如果允许这些操作,可能会导致 UI 出现令人困惑的错误和不一致。useEffect Hook 可以避免这些“副作用”,让 UI 平稳运行。它将 componentDidMount、componentDidUpdate 和 componentWillUnmount 合并在了一处。

useContext

useContext Hook 接受一个上下文对象,也就是 React.createContext 返回的值,并返回该上下文当前的值。useContext Hook 还可以与 React Context API 配合使用,在整个应用中共享数据,而无需将应用的 props 层层传递下去。

需要注意的是,传递给 useContext Hook 的参数必须是上下文对象本身;每当上下文值发生变化,调用 useContext 的组件都会重新渲染。

useReducer

useReducer Hook 是 setState 的一种替代方案。当状态逻辑较复杂、涉及多个子值,或者下一个状态取决于前一个状态时,它尤其适用。它接收一个 reducer 函数和初始状态作为输入,并通过数组解构返回当前状态和一个 dispatch 函数。useReducer 还能优化会触发深层更新的组件的性能。

优点
代码行数更少

Hooks 让你可以按关注点和功能对代码进行分组,而非按生命周期分组。这不仅让代码更整洁、简洁,也减少了代码行数。下面对比了一个使用 React 编写的可搜索产品数据表状态组件,以及使用 useState 关键字后通过 Hooks 实现的写法。

有状态组件
class TweetSearchResults extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: "",
      inThisLocation: false,
    };

    this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
    this.handleInThisLocationChange =
      this.handleInThisLocationChange.bind(this);
  }

  handleFilterTextChange(filterText) {
    this.setState({
      filterText: filterText,
    });
  }

  handleInThisLocationChange(inThisLocation) {
    this.setState({
      inThisLocation: inThisLocation,
    });
  }

  render() {
    return (
      <div>
        <SearchBar
          filterText={this.state.filterText}
          inThisLocation={this.state.inThisLocation}
          onFilterTextChange={this.handleFilterTextChange}
          onInThisLocationChange={this.handleInThisLocationChange}
        />
        <TweetList
          tweets={this.props.tweets}
          filterText={this.state.filterText}
          inThisLocation={this.state.inThisLocation}
        />
      </div>
    );
  }
}

使用 Hooks 实现的同一组件

const TweetSearchResults = ({ tweets }) => {
  const [filterText, setFilterText] = useState("");
  const [inThisLocation, setInThisLocation] = useState(false);
  return (
    <div>
      <SearchBar
        filterText={filterText}
        inThisLocation={inThisLocation}
        setFilterText={setFilterText}
        setInThisLocation={setInThisLocation}
      />
      <TweetList
        tweets={tweets}
        filterText={filterText}
        inThisLocation={inThisLocation}
      />
    </div>
  );
};
简化复杂组件

JavaScript 类可能难以管理、难以配合热重载使用,也可能无法很好地进行压缩。React Hooks 解决了这些问题,让函数式编程更容易实现。采用 Hooks 后,我们无需再使用类组件。

复用有状态逻辑

JavaScript 中的类鼓励多层继承,这会迅速增加整体复杂度和潜在错误。不过,Hooks 让你无需编写类即可使用状态和其他 React 功能。借助 React,你可以始终复用有状态逻辑,无需反复重写代码。这能减少出错的可能性,并支持使用普通函数进行组合。

共享非视觉逻辑

在 Hooks 实现之前,React 无法提取和共享非视觉逻辑。这最终导致了更多复杂性,例如为解决常见问题而采用 HOC 模式和 Render props。不过,Hooks 的引入解决了这一问题,因为它允许将有状态逻辑提取到一个简单的 JavaScript 函数中。

当然,使用 Hooks 时也有一些潜在缺点需要牢记:

  • 必须遵守其规则;如果没有 linter 插件,就很难知道违反了哪条规则。
  • 需要投入相当多的时间练习才能正确使用(例如:useEffect)。
  • 要注意避免错误使用(例如:useCallback、useMemo)。
React Hooks 与类组件

React 引入 Hooks 后,出现了一个新问题:我们该如何判断何时使用带 Hooks 的函数组件,何时使用类组件?借助 Hooks,即使在函数组件中,也可以使用状态和部分生命周期 Hooks。Hooks 还允许你使用本地状态和其他 React 功能,而无需编写类。

以下是 Hooks 和类组件的一些区别,可帮助你做出选择:

React Hooks类组件
有助于避免多层级结构,让代码更清晰通常,使用 HOC 或 renderProps 时,如果想在 DevTools 中查看应用,就必须将应用重构为多层级结构
让 React 组件之间更加统一。由于需要理解绑定以及函数的调用上下文,类组件会让人和机器都感到困惑。

来源

评论

支持 Markdown