一句话定义
自定义 Hook 是一个名字以 use 开头、内部可以调用其他 Hook 的函数——它复用的是「带状态的行为逻辑」,而不是 UI;每次调用得到彼此独立的一套状态。
为什么重要
组件复用解决「长得像」,自定义 Hook 解决「行为像」:拉取数据、订阅主题、防抖、轮询、本地存储……这些横切逻辑一旦散落在组件里,就是重复与 bug 的温床。它是 React 生态第三方 Hook 库(react-use、ahooks 等)得以存在的基石。
前置知识
核心概念
- 命名约定即 API:
use前缀让 lint 与读者都明白「内部含 Hook,有状态、有快照语义」。 - 每次调用独立:两个组件各调一次
useToggle(),状态互不相干(因为 Hook 状态挂在各自的组件实例上)。 - 组合即复用:自定义 Hook 内部自由组合 useState/useEffect/useRef,对调用方只暴露「值 + 行为」。
- 返回形状:数组(
[value, fn],像 useState)或对象(多个值时更易读)。 - 不共享数据,共享行为:要共享数据用 Context(kp-012)或状态库(kp-021)。
原理 / 机制
把 kp-010 的 usePersistentState 升级为「带过期与容错」的成品,展示成熟 Hook 的设计过程:
jsxexport function usePersistentState(key, initial, { validate } = {}) {
const [value, setValue] = useState(() => {
try {
const raw = localStorage.getItem(key);
if (raw === null) return initial;
const parsed = JSON.parse(raw);
return validate ? validate(parsed) ?? initial : parsed; // 脏数据回退
} catch {
return initial; // JSON 解析失败回退
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch { /* 存储满/隐私模式:静默降级 */ }
}, [key, value]);
return [value, setValue];
}
设计要点清单(写一个 Hook 前过一遍):
- 参数最小化:调用方不该理解内部细节。
- 边界处理内聚:脏数据、异常、竞态在 Hook 内消化。
- 返回稳定形状:数组或对象,别混。
- 命名表达「行为」而非「实现」:
useOnlineStatus而不是useWindowEvents。 - 文档写清副作用与清理语义。
跨 Hook 组合(Hook 调 Hook):
jsxfunction useDebouncedValue(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
// 组合:搜索 = 持久化关键词 + 防抖请求
function useSearch(initial) {
const [q, setQ] = usePersistentState('search.q', initial);
const debouncedQ = useDebouncedValue(q, 300);
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
let ignore = false;
setLoading(true);
fetch(`/api/search?q=${encodeURIComponent(debouncedQ)}`)
.then(r => r.json())
.then(data => { if (!ignore) { setResults(data); setLoading(false); } });
return () => { ignore = true; setLoading(false); };
}, [debouncedQ]);
return { q, setQ, results, loading };
}
直观类比
自定义 Hook 像装修队分包:水电(数据请求)、油漆(样式订阅)、门窗(事件监听)各自是专业队伍(Hook),你家(组件)只负责验收结果(调用拿到状态)。两户人家(两个组件实例)各请各的施工队,互不串工——「独立状态、共享工艺」。
实例 / 案例
拆分一个「仪表盘组件」:原来 200 行混合了轮询、键盘快捷键、图表配色;抽出三个 Hook 后组件只剩 JSX 与布局——
usePolling(url, ms):定时拉数据,页面隐藏时暂停(visibility API)。useHotkeys(map):键盘映射集中管理。useThemeTokens():从 Context 拿主题派生 CSS 变量。
组件从「逻辑面条」变成「编排层」,且三个 Hook 都可以单测(kp-027)。
常见误区
- 自定义 Hook 里返回 JSX:那就不是 Hook,是组件(
useCard()返回 JSX → 改写成<Card/>)。 - 以为 Hook 之间共享状态:
useUser()在两个组件各调一次会发两次请求——要共享数据得配 Context 或外部 store。 - 过度抽象:只用一次、且只有两行的逻辑不值得抽 Hook;抽象要等「第三个使用者」出现。
- 忘了清理或竞态守卫:Hook 内的 effect 问题会被 N 个组件放大;抽 Hook 前先确认基础 effect 写对了(kp-010)。
- 参数是不稳定引用导致 effect 疯狂重跑:调用方传内联对象/函数 → Hook 内依赖数组每次都变;在 Hook 内用 useMemo 归一,或文档要求调用方保证引用稳定。
自测题
- 「两个组件调用同一个
useToggle(),状态为什么互不影响」?用 Hooks 链表(kp-014)解释。 - 把 kp-011 的受控表单逻辑抽成
useForm(initial),设计其返回值形状。 - 自定义 Hook 与普通工具函数(
formatDate())的本质区别是什么? useSearch里如果调用方每次渲染都传新的initial,会发生什么?如何防御?
与其他知识点的关系
- 向前:是 kp-010/kp-012/kp-016 等模式的「复用外壳」。
- 向后:kp-016 的 reducer 常被打包进 Hook;kp-017 的 ref 模式常封装为
useEventListener类工具;kp-021 的数据层(TanStack Query)本质是「巨型数据 Hook」;kp-027 中 Hook 可脱离组件直接测试。
延伸阅读
- Reusing Logic with Custom Hooks:<https://react.dev/learn/reusing-logic-with-custom-hooks>
- ahooks(社区 Hook 库范例):<https://ahooks.js.org/>