Kodokon kodokon.com

自定义 hook:抽取并复用逻辑

把带状态的逻辑抽取成可复用、可组合、可测试的 hook。

8 分钟 · 3 题

在 Kodokon 中打开本课

自定义 hook 是一个名字以 use 开头、并且调用其他 hook 的函数。它抽取的是带状态的逻辑,而不是共享的状态:每个调用它的组件都会得到自己独立的 useStateuseEffect,彼此完全隔离。它是 React 中最直接的逻辑复用工具——在 HOC 和 render props 往树里添加层级的地方,hook 直接融进组件的函数体,易于组合,也易于测试。

JAVASCRIPT
import { useEffect, useState } from "react";

function useDebouncedValue(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => {
      setDebounced(value);
    }, delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
清理函数在每次变化时取消上一个定时器。

use 前缀不是装饰性的:正是它让 react-hooks 这个 ESLint 插件能在你的函数内部检查 hook 规则——无条件调用、置于顶层。同样要注意返回的 API 的形态:当用法与 useState 相似时用 [value, setValue] 元组,一旦要暴露的东西超过两个,就用带命名键的对象。

JAVASCRIPT
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key);
    return stored !== null
      ? JSON.parse(stored)
      : initialValue;
  });

  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
}
惰性初始化:localStorage 只在挂载时读取一次。

hook 是可组合的:一个 hook 可以调用其他 hook,包括你自己写的。可读的业务抽象正是这样在通用的基础构件之上涌现出来的。注意 AbortController 与清理函数的配对:没有它,一次为较早输入发出的请求可能会在最新请求之后返回并覆盖其结果——这正是搜索框那经典的竞态条件(race condition)

JAVASCRIPT
function useUserSearch(query) {
  const debounced = useDebouncedValue(query, 400);
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (debounced === "") {
      setResults([]);
      return;
    }
    const controller = new AbortController();
    fetch("/api/search?q=" + debounced, {
      signal: controller.signal,
    })
      .then((res) => res.json())
      .then(setResults)
      .catch(() => {});
    return () => controller.abort();
  }, [debounced]);
  return results;
}
业务 hook 构建在通用 hook 之上。

知识检测

确认你已牢记本课的重点内容。

  1. 自定义 hook 的 use 前缀实际上是做什么用的?
    • 它只是一个从 class 继承下来的纯装饰性约定
    • 它让 linter 把这个函数识别为 hook,并在其内部检查 hook 规则
    • 没有这个前缀,React 会拒绝运行一个带状态的函数
    • 它触发从 react 的自动导入
  2. 两个组件用相同的 key 调用 useLocalStorage。它们会共享状态吗?
    • 会:这个 hook 会自动同步它所有的调用方
    • 不会,而且数据会被存到两个不同的存储 key 下
    • 不会:每次调用都拥有自己的 React 状态;只有 localStorage 里的数据是共享的
  3. 在 useDebouncedValue 中,useEffect 返回的清理函数起什么作用?
    • 取消待执行的定时器,以便在延迟之后只发布最后输入的那个值
    • 每次组件卸载时把 debounced 值重置
    • 在整个延迟期间阻止组件的重新渲染