使用自定义钩子重用逻辑

React 带有几个内置的钩子,如 useStateuseContextuseEffect。有时,你会希望有一个钩子用于更具体的目的:例如,获取数据、跟踪用户是否在线或连接到聊天室。你可能在 React 中找不到这些钩子,但你可以根据应用的需要创建自己的钩子。

¥React comes with several built-in Hooks like useState, useContext, and useEffect. Sometimes, you’ll wish that there was a Hook for some more specific purpose: for example, to fetch data, to keep track of whether the user is online, or to connect to a chat room. You might not find these Hooks in React, but you can create your own Hooks for your application’s needs.

你将学习到

  • 什么是自定义钩子,以及如何编写自己的钩子

    ¥What custom Hooks are, and how to write your own

  • 如何重用组件之间的逻辑

    ¥How to reuse logic between components

  • 如何命名和构造你的自定义钩子

    ¥How to name and structure your custom Hooks

  • 何时以及为何提取自定义钩子

    ¥When and why to extract custom Hooks

自定义钩子:组件之间共享逻辑

¥Custom Hooks: Sharing logic between components

想象一下,你正在开发一个严重依赖网络的应用(就像大多数应用一样)。如果用户在使用你的应用时网络连接意外断开,你想警告他们。你会怎么做?似乎你的组件中需要两件事:

¥Imagine you’re developing an app that heavily relies on the network (as most apps do). You want to warn the user if their network connection has accidentally gone off while they were using your app. How would you go about it? It seems like you’ll need two things in your component:

  1. 跟踪网络是否在线的一种状态。

    ¥A piece of state that tracks whether the network is online.

  2. 订阅全局 onlineoffline 事件并更新该状态的副作用。

    ¥An Effect that subscribes to the global online and offline events, and updates that state.

这将使你的组件 同步 保持网络状态。你可以从这样的事情开始:

¥This will keep your component synchronized with the network status. You might start with something like this:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

尝试打开和关闭你的网络,并注意此 StatusBar 如何响应你的操作进行更新。

¥Try turning your network on and off, and notice how this StatusBar updates in response to your actions.

现在假设你还想在不同的组件中使用相同的逻辑。你想要实现一个保存按钮,当网络关闭时,该按钮将被禁用并显示 “重新连接…” 而不是 “保存”。

¥Now imagine you also want to use the same logic in a different component. You want to implement a Save button that will become disabled and show “Reconnecting…” instead of “Save” while the network is off.

首先,你可以将 isOnline 状态和副作用复制并粘贴到 SaveButton 中:

¥To start, you can copy and paste the isOnline state and the Effect into SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

验证如果你关闭网络,按钮将改变其外观。

¥Verify that, if you turn off the network, the button will change its appearance.

这两个组件工作正常,但不幸的是它们之间的逻辑重复。看起来即使它们具有不同的视觉外观,你仍想重用它们之间的逻辑。

¥These two components work fine, but the duplication in logic between them is unfortunate. It seems like even though they have different visual appearance, you want to reuse the logic between them.

从组件中提取你自己的自定义钩子

¥Extracting your own custom Hook from a component

想象一下,类似于 useStateuseEffect,有一个内置的 useOnlineStatus 钩子。然后这两个组件都可以简化,你可以删除它们之间的重复:

¥Imagine for a moment that, similar to useState and useEffect, there was a built-in useOnlineStatus Hook. Then both of these components could be simplified and you could remove the duplication between them:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progress saved');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}

虽然没有这个内置的钩子,但是你可以自己写。声明一个名为 useOnlineStatus 的函数,并将你之前编写的组件中的所有重复代码移至其中:

¥Although there is no such built-in Hook, you can write it yourself. Declare a function called useOnlineStatus and move all the duplicated code into it from the components you wrote earlier:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

在函数结束时,返回 isOnline。这让你的组件读取该值:

¥At the end of the function, return isOnline. This lets your components read that value:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

确认打开和关闭网络会更新这两个组件。

¥Verify that switching the network on and off updates both components.

现在你的组件没有那么多重复逻辑。更重要的是,它们内部的代码描述了它们想要做什么(使用在线状态!),而不是如何做(通过订阅浏览器事件)。

¥Now your components don’t have as much repetitive logic. More importantly, the code inside them describes what they want to do (use the online status!) rather than how to do it (by subscribing to the browser events).

当你将逻辑提取到自定义钩子中时,你可以隐藏有关如何处理某些外部系统或浏览器 API 的粗糙细节。你的组件的代码表达了你的意图,而不是实现。

¥When you extract logic into custom Hooks, you can hide the gnarly details of how you deal with some external system or a browser API. The code of your components expresses your intent, not the implementation.

钩子名称始终以 use 开头

¥Hook names always start with use

React 应用是从组件构建的。组件是从钩子构建的,无论是内置的还是自定义的。你可能会经常使用其他人创建的自定义钩子,但有时你可能会自己编写一个!

¥React applications are built from components. Components are built from Hooks, whether built-in or custom. You’ll likely often use custom Hooks created by others, but occasionally you might write one yourself!

你必须遵循以下命名约定:

¥You must follow these naming conventions:

  1. React 组件名称必须以大写字母开头,例如 StatusBarSaveButton。React 组件还需要返回一些 React 知道如何显示的东西,比如一段 JSX。

    ¥React component names must start with a capital letter, like StatusBar and SaveButton. React components also need to return something that React knows how to display, like a piece of JSX.

  2. 钩子名称必须以 use 开头,后跟大写字母,例如 useState(内置)或 useOnlineStatus(自定义,如页面前面的内容)。钩子可以返回任意值。

    ¥Hook names must start with use followed by a capital letter, like useState (built-in) or useOnlineStatus (custom, like earlier on the page). Hooks may return arbitrary values.

此约定保证你始终可以查看组件并知道它的状态、副作用和其他 React 功能可能在哪里 “hide”。例如,如果你在组件中看到 getColor() 函数调用,你可以确定它不可能在内部包含 React 状态,因为它的名称不是以 use 开头。但是,像 useOnlineStatus() 这样的函数调用很可能会包含对内部其他钩子的调用!

¥This convention guarantees that you can always look at a component and know where its state, Effects, and other React features might “hide”. For example, if you see a getColor() function call inside your component, you can be sure that it can’t possibly contain React state inside because its name doesn’t start with use. However, a function call like useOnlineStatus() will most likely contain calls to other Hooks inside!

注意

如果你的 linter 是 为 React 配置,,它将强制执行此命名约定。向上滚动到上面的沙盒并将 useOnlineStatus 重命名为 getOnlineStatus。请注意,linter 将不再允许你在其中调用 useStateuseEffect。只有钩子和组件才能调用其他钩子!

¥If your linter is configured for React, it will enforce this naming convention. Scroll up to the sandbox above and rename useOnlineStatus to getOnlineStatus. Notice that the linter won’t allow you to call useState or useEffect inside of it anymore. Only Hooks and components can call other Hooks!

深入研究

渲染期间调用的所有函数是否都应以 use 前缀开头?

¥Should all functions called during rendering start with the use prefix?

不。不调用钩子的函数不需要是钩子。

¥No. Functions that don’t call Hooks don’t need to be Hooks.

如果你的函数不调用任何钩子,请避免使用 use 前缀。而是,将其编写为不带 use 前缀的常规函数。比如下面的 useSorted 就没有调用钩子,所以改用 getSorted

¥If your function doesn’t call any Hooks, avoid the use prefix. Instead, write it as a regular function without the use prefix. For example, useSorted below doesn’t call Hooks, so call it getSorted instead:

// 🔴 Avoid: A Hook that doesn't use Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Good: A regular function that doesn't use Hooks
function getSorted(items) {
return items.slice().sort();
}

这确保你的代码可以在任何地方调用此常规函数,包括以下条件:

¥This ensures that your code can call this regular function anywhere, including conditions:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ It's ok to call getSorted() conditionally because it's not a Hook
displayedItems = getSorted(items);
}
// ...
}

如果函数内部至少使用了一个钩子,你应该给函数加上 use 前缀(从而使它成为一个钩子):

¥You should give use prefix to a function (and thus make it a Hook) if it uses at least one Hook inside of it:

// ✅ Good: A Hook that uses other Hooks
function useAuth() {
return useContext(Auth);
}

从技术上讲,这不是由 React 强制执行的。原则上,你可以创建一个不调用其他钩子的钩子。这通常会造成混淆和限制,因此最好避免这种模式。但是,在极少数情况下它可能会有帮助。例如,也许你的函数现在没有使用任何钩子,但你计划在将来向它添加一些钩子调用。然后用 use 前缀命名它是有意义的:

¥Technically, this isn’t enforced by React. In principle, you could make a Hook that doesn’t call other Hooks. This is often confusing and limiting so it’s best to avoid that pattern. However, there may be rare cases where it is helpful. For example, maybe your function doesn’t use any Hooks right now, but you plan to add some Hook calls to it in the future. Then it makes sense to name it with the use prefix:

// ✅ Good: A Hook that will likely use some other Hooks later
function useAuth() {
// TODO: Replace with this line when authentication is implemented:
// return useContext(Auth);
return TEST_USER;
}

那么组件将无法有条件地调用它。当你实际在其中添加钩子调用时,这将变得很重要。如果你不打算在其中使用钩子(现在或以后),请不要将其设为钩子。

¥Then components won’t be able to call it conditionally. This will become important when you actually add Hook calls inside. If you don’t plan to use Hooks inside it (now or later), don’t make it a Hook.

自定义钩子让你共享状态逻辑,而不是状态本身

¥Custom Hooks let you share stateful logic, not state itself

在前面的示例中,当你打开和关闭网络时,两个组件会一起更新。但是,认为它们之间共享单个 isOnline 状态变量的想法是错误的。看看这段代码:

¥In the earlier example, when you turned the network on and off, both components updated together. However, it’s wrong to think that a single isOnline state variable is shared between them. Look at this code:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

它的工作方式与提取重复之前相同:

¥It works the same way as before you extracted the duplication:

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

这是两个完全独立的状态变量和副作用!它们恰好同时具有相同的值,因为你将它们与相同的外部值同步(无论网络是否打开)。

¥These are two completely independent state variables and Effects! They happened to have the same value at the same time because you synchronized them with the same external value (whether the network is on).

为了更好地说明这一点,我们需要一个不同的例子。考虑这个 Form 组件:

¥To better illustrate this, we’ll need a different example. Consider this Form component:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

每个表单字段都有一些重复的逻辑:

¥There’s some repetitive logic for each form field:

  1. 有一个状态(firstNamelastName)。

    ¥There’s a piece of state (firstName and lastName).

  2. 有一个更改处理程序(handleFirstNameChangehandleLastNameChange)。

    ¥There’s a change handler (handleFirstNameChange and handleLastNameChange).

  3. 有一段 JSX 指定了该输入的 valueonChange 属性。

    ¥There’s a piece of JSX that specifies the value and onChange attributes for that input.

你可以将重复逻辑提取到这个 useFormInput 自定义钩子中:

¥You can extract the repetitive logic into this useFormInput custom Hook:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

请注意,它只声明了一个名为 value 的状态变量。

¥Notice that it only declares one state variable called value.

但是,Form 组件调用了 useFormInput 两次:

¥However, the Form component calls useFormInput two times:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

这就是为什么它像声明两个独立的状态变量一样工作!

¥This is why it works like declaring two separate state variables!

自定义钩子允许你共享状态逻辑,但不能共享状态本身。对钩子的每次调用完全独立于对同一钩子的所有其他调用。这就是为什么上面的两个沙箱是完全等价的。如果你愿意,请向上滚动并比较它们。提取自定义钩子前后的行为是相同的。

¥Custom Hooks let you share stateful logic but not state itself. Each call to a Hook is completely independent from every other call to the same Hook. This is why the two sandboxes above are completely equivalent. If you’d like, scroll back up and compare them. The behavior before and after extracting a custom Hook is identical.

当你需要在多个组件之间共享状态本身时,请使用 把它举起来传递下去

¥When you need to share the state itself between multiple components, lift it up and pass it down instead.

在钩子之间传递反应值

¥Passing reactive values between Hooks

自定义钩子中的代码将在每次重新渲染组件时重新运行。这就是为什么像组件一样自定义钩子 需要纯粹。 将自定义钩子的代码视为组件主体的一部分!

¥The code inside your custom Hooks will re-run during every re-render of your component. This is why, like components, custom Hooks need to be pure. Think of custom Hooks’ code as part of your component’s body!

因为自定义钩子与你的组件一起重新渲染,所以它们总是收到最新的属性和状态。要了解这意味着什么,请考虑这个聊天室示例。更改服务器 URL 或聊天室:

¥Because custom Hooks re-render together with your component, they always receive the latest props and state. To see what this means, consider this chat room example. Change the server URL or the chat room:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

当你更改 serverUrlroomId 时,副作用器 “reacts” 对你的改变 将重新同步。你可以通过控制台消息得知,每次你更改副作用的依赖时,聊天都会重新连接。

¥When you change serverUrl or roomId, the Effect “reacts” to your changes and re-synchronizes. You can tell by the console messages that the chat re-connects every time that you change your Effect’s dependencies.

现在将副作用的代码移动到自定义钩子中:

¥Now move the Effect’s code into a custom Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

这让你的 ChatRoom 组件调用你的自定义钩子而不必担心它在内部是如何工作的:

¥This lets your ChatRoom component call your custom Hook without worrying about how it works inside:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
Server URL:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}

这看起来简单多了!(但它做同样的事情。)

¥This looks much simpler! (But it does the same thing.)

请注意,逻辑仍然响应属性和状态的变化。尝试编辑服务器 URL 或所选房间:

¥Notice that the logic still responds to prop and state changes. Try editing the server URL or the selected room:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

注意你是如何获取一个钩子的返回值的:

¥Notice how you’re taking the return value of one Hook:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

并将其作为输入传递给另一个钩子:

¥and pass it as an input to another Hook:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

每次你的 ChatRoom 组件重新渲染时,它都会将最新的 roomIdserverUrl 传递给你的钩子。这就是为什么只要重新渲染后它们的值不同,你的副作用就会重新连接到聊天。(如果你曾经使用过音频或视频处理软件,像这样链接钩子可能会让你想起链接视觉或音频效果。就好像 useState “进入” 的输出是 useChatRoom 的输入一样。)

¥Every time your ChatRoom component re-renders, it passes the latest roomId and serverUrl to your Hook. This is why your Effect re-connects to the chat whenever their values are different after a re-render. (If you ever worked with audio or video processing software, chaining Hooks like this might remind you of chaining visual or audio effects. It’s as if the output of useState “feeds into” the input of the useChatRoom.)

将事件处理程序传递给自定义钩子

¥Passing event handlers to custom Hooks

开发中

本节介绍尚未在 React 稳定版本中发布的实验性 API。

¥This section describes an experimental API that has not yet been released in a stable version of React.

当你开始在更多组件中使用 useChatRoom 时,你可能希望让组件自定义其行为。例如,目前,消息到达时执行的操作的逻辑被硬编码在钩子中:

¥As you start using useChatRoom in more components, you might want to let components customize its behavior. For example, currently, the logic for what to do when a message arrives is hardcoded inside the Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

假设你想将此逻辑移回你的组件:

¥Let’s say you want to move this logic back to your component:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...

要使其工作,请更改你的自定义钩子以将 onReceiveMessage 作为其命名选项之一:

¥To make this work, change your custom Hook to take onReceiveMessage as one of its named options:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}

这会起作用,但是当你的自定义钩子接受事件处理程序时,你还可以进行另一项改进。

¥This will work, but there’s one more improvement you can do when your custom Hook accepts event handlers.

添加对 onReceiveMessage 的依赖并不理想,因为每次组件重新渲染时都会导致聊天重新连接。将此事件处理程序封装到副作用事件中以将其从依赖中删除:

¥Adding a dependency on onReceiveMessage is not ideal because it will cause the chat to re-connect every time the component re-renders. Wrap this event handler into an Effect Event to remove it from the dependencies:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
}

现在,每次 ChatRoom 组件重新渲染时聊天都不会重新连接。这是将事件处理程序传递给你可以使用的自定义钩子的完整工作演示:

¥Now the chat won’t re-connect every time that the ChatRoom component re-renders. Here is a fully working demo of passing an event handler to a custom Hook that you can play with:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

请注意,你不再需要了解 useChatRoom 的工作原理即可使用它。你可以将它添加到任何其他组件,传递任何其他选项,并且它会以相同的方式工作。这就是自定义钩子的强大之处。

¥Notice how you no longer need to know how useChatRoom works in order to use it. You could add it to any other component, pass any other options, and it would work the same way. That’s the power of custom Hooks.

何时使用自定义钩子

¥When to use custom Hooks

你不需要为每一段重复的代码提取自定义钩子。一些重复是好的。例如,提取 useFormInput 钩子来封装单个 useState 调用可能是不必要的。

¥You don’t need to extract a custom Hook for every little duplicated bit of code. Some duplication is fine. For example, extracting a useFormInput Hook to wrap a single useState call like earlier is probably unnecessary.

但是,每当你编写副作用时,请考虑将其封装在自定义钩子中是否会更清晰。你不应该经常需要副作用, 所以如果你正在写一个,这意味着你需要 “走出 React” 来与一些外部系统同步或者做一些 React 没有内置 API 的事情。将其封装到自定义钩子中可以让你准确地传达你的意图以及数据如何流经它。

¥However, whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook. You shouldn’t need Effects very often, so if you’re writing one, it means that you need to “step outside React” to synchronize with some external system or to do something that React doesn’t have a built-in API for. Wrapping it into a custom Hook lets you precisely communicate your intent and how the data flows through it.

例如,考虑显示两个下拉列表的 ShippingForm 组件:一个显示城市列表,另一个显示所选城市的区域列表。你可能会从一些看起来像这样的代码开始:

¥For example, consider a ShippingForm component that displays two dropdowns: one shows the list of cities, and another shows the list of areas in the selected city. You might start with some code that looks like this:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// This Effect fetches cities for a country
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// This Effect fetches areas for the selected city
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

虽然这段代码相当重复,但 将这些副作用彼此分开是正确的。 它们同步两个不同的东西,所以你不应该将它们合并到一个副作用中。而是,你可以通过将它们之间的通用逻辑提取到你自己的 useData 钩子中来简化上面的 ShippingForm 组件:

¥Although this code is quite repetitive, it’s correct to keep these Effects separate from each other. They synchronize two different things, so you shouldn’t merge them into one Effect. Instead, you can simplify the ShippingForm component above by extracting the common logic between them into your own useData Hook:

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

现在你可以将 ShippingForm 组件中的两种副作用替换为对 useData 的调用:

¥Now you can replace both Effects in the ShippingForm components with calls to useData:

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

提取自定义钩子使数据流显式。你将 url 送入,将 data 送出。通过 “hiding” 你的副作用在 useData 中,你也可以防止在 ShippingForm 组件上工作的人将 不必要的依赖 添加到它。随着时间的推移,你应用的大部分副作用都将在自定义钩子中。

¥Extracting a custom Hook makes the data flow explicit. You feed the url in and you get the data out. By “hiding” your Effect inside useData, you also prevent someone working on the ShippingForm component from adding unnecessary dependencies to it. With time, most of your app’s Effects will be in custom Hooks.

深入研究

让你的自定义钩子专注于具体的高级用例

¥Keep your custom Hooks focused on concrete high-level use cases

首先选择自定义钩子的名称。如果你难以选择一个清晰的名称,则可能意味着你的副作用与组件逻辑的其余部分过于耦合,并且尚未准备好提取。

¥Start by choosing your custom Hook’s name. If you struggle to pick a clear name, it might mean that your Effect is too coupled to the rest of your component’s logic, and is not yet ready to be extracted.

理想情况下,你的自定义钩子的名称应该足够清晰,即使不经常编写代码的人也可以很好地猜测你的自定义钩子的作用、获取的内容以及返回的内容:

¥Ideally, your custom Hook’s name should be clear enough that even a person who doesn’t write code often could have a good guess about what your custom Hook does, what it takes, and what it returns:

  • useData(url)

  • useImpressionLog(eventName, extraData)

  • useChatRoom(options)

当你与外部系统同步时,你的自定义钩子名称可能更具技术性并使用特定于该系统的行话。只要熟悉该系统的人清楚就可以了:

¥When you synchronize with an external system, your custom Hook name may be more technical and use jargon specific to that system. It’s good as long as it would be clear to a person familiar with that system:

  • useMediaQuery(query)

  • useSocket(url)

  • useIntersectionObserver(ref, options)

让自定义钩子专注于具体的高级用例。避免创建和使用自定义 “lifecycle” 钩子作为 useEffect API 本身的替代和便利封装器:

¥Keep custom Hooks focused on concrete high-level use cases. Avoid creating and using custom “lifecycle” Hooks that act as alternatives and convenience wrappers for the useEffect API itself:

  • 🔴 useMount(fn)

  • 🔴 useEffectOnce(fn)

  • 🔴 useUpdateEffect(fn)

例如,这个 useMount 钩子试图确保一些代码只运行 “挂载时”:

¥For example, this useMount Hook tries to ensure some code only runs “on mount”:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔴 Avoid: using custom "lifecycle" Hooks
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Avoid: creating custom "lifecycle" Hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'fn'
}

useMount 这样的自定义 “lifecycle” 钩子不太适合 React 范式。例如,此代码示例有一个错误(它没有将 “react” 更改为 roomIdserverUrl),但 linter 不会就此向你发出警告,因为 linter 仅检查直接的 useEffect 调用。它不会知道你的钩子。

¥Custom “lifecycle” Hooks like useMount don’t fit well into the React paradigm. For example, this code example has a mistake (it doesn’t “react” to roomId or serverUrl changes), but the linter won’t warn you about it because the linter only checks direct useEffect calls. It won’t know about your Hook.

如果你正在编写副作用,请直接使用 React API 开始:

¥If you’re writing an Effect, start by using the React API directly:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Good: two raw Effects separated by purpose

useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

然后,你可以(但不必)为不同的高级用例提取自定义钩子:

¥Then, you can (but don’t have to) extract custom Hooks for different high-level use cases:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Great: custom Hooks named after their purpose
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

一个好的自定义钩子通过限制调用代码的作用,使调用代码更具声明性。例如,useChatRoom(options) 只能连接到聊天室,而 useImpressionLog(eventName, extraData) 只能将印象日志发送到分析。如果你的自定义钩子 API 不限制用例并且非常抽象,那么从长远来看,它可能会引入比解决的问题更多的问题。

¥A good custom Hook makes the calling code more declarative by constraining what it does. For example, useChatRoom(options) can only connect to the chat room, while useImpressionLog(eventName, extraData) can only send an impression log to the analytics. If your custom Hook API doesn’t constrain the use cases and is very abstract, in the long run it’s likely to introduce more problems than it solves.

自定义钩子帮助你迁移到更好的模式

¥Custom Hooks help you migrate to better patterns

副作用是 “应急方案”:当你需要 “走出 React” 并且没有更好的内置解决方案适合你的用例时,你可以使用它们。随着时间的推移,React 团队的目标是通过为更具体的问题提供更具体的解决方案,将应用中的副作用数量减少到最少。在这些解决方案可用时,将你的副作用封装在自定义钩子中可以更轻松地升级你的代码。

¥Effects are an “escape hatch”: you use them when you need to “step outside React” and when there is no better built-in solution for your use case. With time, the React team’s goal is to reduce the number of the Effects in your app to the minimum by providing more specific solutions to more specific problems. Wrapping your Effects in custom Hooks makes it easier to upgrade your code when these solutions become available.

让我们回到这个例子:

¥Let’s return to this example:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

在上面的示例中,useOnlineStatus 是通过一对 useStateuseEffect 实现的,但这并不是最佳解决方案。它没有考虑许多 Edge 情况。例如,它假设当组件挂载时,isOnline 已经是 true,但如果网络已经下线,这可能是错误的。你可以使用浏览器 navigator.onLine API 来检查它,但直接使用它在服务器上无法生成初始 HTML。简而言之,这段代码可以改进。

¥In the above example, useOnlineStatus is implemented with a pair of useState and useEffect. However, this isn’t the best possible solution. There is a number of edge cases it doesn’t consider. For example, it assumes that when the component mounts, isOnline is already true, but this may be wrong if the network already went offline. You can use the browser navigator.onLine API to check for that, but using it directly would not work on the server for generating the initial HTML. In short, this code could be improved.

幸运的是,React 18 包含一个名为 useSyncExternalStore 的专用 API,它可以为你解决所有这些问题。以下是你的 useOnlineStatus 钩子重写以利用这个新 API 的方式:

¥Luckily, React 18 includes a dedicated API called useSyncExternalStore which takes care of all of these problems for you. Here is how your useOnlineStatus Hook, rewritten to take advantage of this new API:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // How to get the value on the client
    () => true // How to get the value on the server
  );
}

请注意,你不需要更改任何组件即可进行此迁移:

¥Notice how you didn’t need to change any of the components to make this migration:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

这是为什么在自定义钩子中封装副作用通常是有益的另一个原因:

¥This is another reason for why wrapping Effects in custom Hooks is often beneficial:

  1. 你使流入和流出副作用的数据非常明确。

    ¥You make the data flow to and from your Effects very explicit.

  2. 你让你的组件专注于意图而不是副作用的确切实现。

    ¥You let your components focus on the intent rather than on the exact implementation of your Effects.

  3. 当 React 添加新功能时,你可以删除这些副作用而无需更改任何组件。

    ¥When React adds new features, you can remove those Effects without changing any of your components.

设计系统, 类似,你可能会发现开始将应用组件中的常用习语提取到自定义钩子中会很有帮助。这将使你的组件代码专注于意图,并让你避免经常编写原始副作用。许多优秀的自定义钩子由 React 社区维护。

¥Similar to a design system, you might find it helpful to start extracting common idioms from your app’s components into custom Hooks. This will keep your components’ code focused on the intent, and let you avoid writing raw Effects very often. Many excellent custom Hooks are maintained by the React community.

深入研究

React 会提供任何内置的数据获取解决方案吗?

¥Will React provide any built-in solution for data fetching?

我们仍在制定细节,但我们希望将来你可以像这样编写数据获取:

¥We’re still working out the details, but we expect that in the future, you’ll write data fetching like this:

import { use } from 'react'; // Not available yet!

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

如果你在你的应用中使用像上面的 useData 这样的自定义钩子,那么与你在每个组件中手动编写原始副作用相比,迁移到最终推荐的方法所需的更改更少。但是,旧方法仍然可以正常工作,所以如果你喜欢编写原始副作用,则可以继续这样做。

¥If you use custom Hooks like useData above in your app, it will require fewer changes to migrate to the eventually recommended approach than if you write raw Effects in every component manually. However, the old approach will still work fine, so if you feel happy writing raw Effects, you can continue to do that.

有不止一种方法可以做到

¥There is more than one way to do it

假设你想使用浏览器 requestAnimationFrame API 从头开始实现淡入动画。你可以从设置动画循环的副作用开始。在动画的每一帧中,你可以更改 保留在引用 的 DOM 节点的不透明度,直到达到 1。你的代码可能像这样开始:

¥Let’s say you want to implement a fade-in animation from scratch using the browser requestAnimationFrame API. You might start with an Effect that sets up an animation loop. During each frame of the animation, you could change the opacity of the DOM node you hold in a ref until it reaches 1. Your code might start like this:

import { useState, useEffect, useRef } from 'react';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // We still have more frames to paint
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

为了使组件更具可读性,你可以将逻辑提取到 useFadeIn 自定义钩子中:

¥To make the component more readable, you might extract the logic into a useFadeIn custom Hook:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

你可以保持 useFadeIn 代码不变,但也可以对其进行更多重构。例如,你可以将用于设置动画循环的逻辑从 useFadeIn 提取到自定义 useAnimationLoop 钩子中:

¥You could keep the useFadeIn code as is, but you could also refactor it more. For example, you could extract the logic for setting up the animation loop out of useFadeIn into a custom useAnimationLoop Hook:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

但是,你不必那样做。与常规函数一样,最终你决定在何处划定代码不同部分之间的界限。你也可以采用非常不同的方法。你可以将大部分命令式逻辑移动到 JavaScript 类: 中,而不是将逻辑保留在副作用中

¥However, you didn’t have to do that. As with regular functions, ultimately you decide where to draw the boundaries between different parts of your code. You could also take a very different approach. Instead of keeping the logic in the Effect, you could move most of the imperative logic inside a JavaScript class:

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

副作用让你可以将 React 连接到外部系统。副作用之间需要的协调越多(例如,链接多个动画),就像在上面的沙箱中一样,完全从副作用和钩子中提取逻辑就越有意义。然后,你提取的代码将成为 “外部系统”。这让你的副作用保持简单,因为它们只需要将消息发送到你在 React 之外移动的系统。

¥Effects let you connect React to external systems. The more coordination between Effects is needed (for example, to chain multiple animations), the more it makes sense to extract that logic out of Effects and Hooks completely like in the sandbox above. Then, the code you extracted becomes the “external system”. This lets your Effects stay simple because they only need to send messages to the system you’ve moved outside React.

上面的示例假设淡入逻辑需要用 JavaScript 编写。然而,这种特殊的淡入动画使用普通 CSS 动画: 实现起来既简单又高效

¥The examples above assume that the fade-in logic needs to be written in JavaScript. However, this particular fade-in animation is both simpler and much more efficient to implement with a plain CSS Animation:

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

有时,你甚至不需要钩子!

¥Sometimes, you don’t even need a Hook!

回顾

  • 自定义钩子让你可以在组件之间共享逻辑。

    ¥Custom Hooks let you share logic between components.

  • 自定义钩子的名称必须以 use 开头,后跟一个大写字母。

    ¥Custom Hooks must be named starting with use followed by a capital letter.

  • Custom 钩子只共享状态逻辑,而不是状态本身。

    ¥Custom Hooks only share stateful logic, not state itself.

  • 你可以将 React 值从一个钩子传递到另一个钩子,并且它们会保持最新。

    ¥You can pass reactive values from one Hook to another, and they stay up-to-date.

  • 每次你的组件重新渲染时,所有钩子都会重新运行。

    ¥All Hooks re-run every time your component re-renders.

  • 你的自定义钩子的代码应该是纯粹的,就像你的组件的代码一样。

    ¥The code of your custom Hooks should be pure, like your component’s code.

  • 将自定义钩子接收到的事件处理程序封装到副作用事件中。

    ¥Wrap event handlers received by custom Hooks into Effect Events.

  • 不要创建像 useMount 这样的自定义钩子。保持它们的目的明确。

    ¥Don’t create custom Hooks like useMount. Keep their purpose specific.

  • 如何以及在何处选择代码边界取决于你。

    ¥It’s up to you how and where to choose the boundaries of your code.

挑战 1 / 5:
提取一个 useCounter 钩子

¥Extract a useCounter Hook

该组件使用一个状态变量和一个副作用来显示一个每秒递增的数字。将此逻辑提取到名为 useCounter 的自定义钩子中。你的目标是使 Counter 组件实现看起来完全像这样:

¥This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:

export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}

你需要在 useCounter.js 中编写自定义钩子并将其导入到 Counter.js 文件中。

¥You’ll need to write your custom Hook in useCounter.js and import it into the Counter.js file.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}


React 中文网 - 粤ICP备13048890号