把带状态的逻辑抽取成可复用、可组合、可测试的 hook。
在 Kodokon 中打开本课自定义 hook 是一个名字以 use 开头、并且调用其他 hook 的函数。它抽取的是带状态的逻辑,而不是共享的状态:每个调用它的组件都会得到自己独立的 useState 和 useEffect,彼此完全隔离。它是 React 中最直接的逻辑复用工具——在 HOC 和 render props 往树里添加层级的地方,hook 直接融进组件的函数体,易于组合,也易于测试。
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] 元组,一旦要暴露的东西超过两个,就用带命名键的对象。
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];
}hook 是可组合的:一个 hook 可以调用其他 hook,包括你自己写的。可读的业务抽象正是这样在通用的基础构件之上涌现出来的。注意 AbortController 与清理函数的配对:没有它,一次为较早输入发出的请求可能会在最新请求之后返回并覆盖其结果——这正是搜索框那经典的竞态条件(race condition)。
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;
}