讲解

自定义 Hook 是 React 逻辑复用的标准机制:一个以 use 开头的普通函数,内部可以调用 useState、useEffect 等内置 Hook,把「带状态的逻辑」封装成可复用单元。它和工具函数的区别:工具函数复用纯计算,自定义 Hook 复用「状态 + 副作用 + 生命周期」的组合——useLocalStorage、useFetch、useDebounce、useMediaQuery 是四大经典。

写一个 Hook 的要点。命名必须 use 开头(React 靠名字识别 Hook,ESLint 才会检查规则)。每次调用独立状态:两个组件各自 useCounter,状态互不相干(Hook 内部每次调用都新建 state)。返回什么?单个值直接返回(useMediaQuery 返回布尔),多个相关值返回对象(useFetch 返回 { data, loading, error }),需要操作返回值返回元组(useState 风格 [value, setValue])。

Hook 之间可以组合:useFetch 内部可以用 useState + useEffect,useSearch 内部可以用 useDebounce + useFetch。复杂逻辑这样一层层搭起来,组件最后只剩一行 use 调用——「组件管渲染,Hook 管逻辑」是 React 应用成熟的标志。测试也方便:Hook 的逻辑可以脱离界面用 @testing-library/react 的 renderHook 或直接断言纯逻辑部分。

示例

实现两个真实可用的 Hook 逻辑(useDebounce 与 useLocalStorage 的核心),全部断言通过(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

// useDebounce 核心:输入停止 300ms 后才更新
function debounce(fn, delay) {
  let timer = null;
  return {
    call(value) {
      clearTimeout(timer); // 新输入取消上一次
      timer = setTimeout(() => fn(value), delay);
    },
    pending: () => timer !== null,
  };
}

const searches = [];
const debounced = debounce((q) => searches.push(q), 300);
debounced.call('r');
debounced.call('re');
debounced.call('rea');
debounced.call('react'); // 只有最后一次会真正执行
await new Promise((r) => setTimeout(r, 350));
assert.deepStrictEqual(searches, ['react']);

// useLocalStorage 核心:读写同步 + JSON 序列化 + 异常兜底
const memoryStore = new Map();
function createLocalStorageHook(key, initial) {
  let value = initial;
  const raw = memoryStore.get(key);
  if (raw !== undefined) {
    try {
      value = JSON.parse(raw); // 解析失败回退初始值
    } catch {
      value = initial;
    }
  }
  return {
    get value() {
      return value;
    },
    setValue(v) {
      value = typeof v === 'function' ? v(value) : v; // 支持函数式更新
      memoryStore.set(key, JSON.stringify(value));
    },
  };
}

const prefs = createLocalStorageHook('prefs', { theme: 'light', fontSize: 14 });
prefs.setValue((p) => ({ ...p, theme: 'dark' }));
const reloaded = createLocalStorageHook('prefs', { theme: 'light', fontSize: 14 });
assert.strictEqual(reloaded.value.theme, 'dark'); // 刷新后恢复

memoryStore.set('broken', '{oops'); // 模拟损坏数据
const broken = createLocalStorageHook('broken', { safe: true });
assert.deepStrictEqual(broken.value, { safe: true }); // 回退初始值

console.log('防抖: 连续输入 4 次只搜索 1 次:', searches.join(''));
console.log('localStorage 恢复主题:', reloaded.value.theme, ';损坏数据安全回退');

真实 Hook 写法:

import { useEffect, useState } from 'react';

function useDebounce(value, delay = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(timer); // 值变了取消上一次
  }, [value, delay]);
  return debounced;
}

function SearchPage() {
  const [keyword, setKeyword] = useState('');
  const debouncedKeyword = useDebounce(keyword, 300);

  useEffect(() => {
    if (debouncedKeyword) console.log('搜索:', debouncedKeyword);
  }, [debouncedKeyword]);

  return <input value={keyword} onChange={(e) => setKeyword(e.target.value)} placeholder="输入即搜" />;
}

常见坑

  • 命名不带 use:不叫 use 开头就不是 Hook,里面调 Hook 会被 ESLint 报错且违反规则。
  • 以为 Hook 共享状态:每个组件调用自己的 Hook,状态各自独立;要共享得状态提升或全局库。
  • Hook 里写死参数:delay、key 这类参数应该可配,并提供合理默认值。
  • 返回数组还是对象没约定:两值元组、多值对象是社区惯例;一个 Hook 返回值类型要稳定。
  • Hook 里漏了清理:内部 useEffect 的定时器/订阅照样要清理,Hook 使用者不该为此操心。

小结

自定义 Hook = use 开头 + 复用带状态逻辑;每次调用独立状态,返回值约定稳定;组合 Hook 搭复杂逻辑。下一章讲 Hook 使用的硬性规则及其背后的实现原理。