讲解
自定义 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 使用的硬性规则及其背后的实现原理。