一句话规则
任何 use 开头的函数,里面调了别的 hook——就是一个自定义 Hook。它本身不是新概念,只是函数。
function useCounter(initial = 0) {
const [count, setCount] = useState(initial);
const inc = () => setCount(c => c + 1);
const reset = () => setCount(initial);
return { count, inc, reset };
}
// 用法
const { count, inc } = useCounter(0);
为什么命名要以 use 开头
不是装饰——是给 ESLint / React 的信号:
- 它会调 hook,遵守 Hooks 规则(必须在组件 / hook 顶层调用)
eslint-plugin-react-hooks据此检查依赖
破坏命名约定 → 静态检查工具失效。
经典例子
useDebounce
function useDebounce<T>(value: T, ms: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), ms);
return () => clearTimeout(t);
}, [value, ms]);
return debounced;
}
// 用法
const debouncedQ = useDebounce(q, 300);
useEffect(() => { search(debouncedQ); }, [debouncedQ]);
useLocalStorage
function useLocalStorage<T>(key: string, initial: T) {
const [val, setVal] = useState<T>(() => {
if (typeof window === 'undefined') return initial;
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(val));
}, [key, val]);
return [val, setVal] as const;
}
useMediaQuery
function useMediaQuery(query: string) {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mql = window.matchMedia(query);
setMatches(mql.matches);
const onChange = (e: MediaQueryListEvent) => setMatches(e.matches);
mql.addEventListener('change', onChange);
return () => mql.removeEventListener('change', onChange);
}, [query]);
return matches;
}
const isMobile = useMediaQuery('(max-width: 768px)');
useFetch(教学版,生产用 TanStack Query)
function useFetch<T>(url: string) {
const [state, setState] = useState<{
data: T | null; loading: boolean; error: Error | null;
}>({ data: null, loading: true, error: null });
useEffect(() => {
let cancelled = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then(r => r.json())
.then(data => { if (!cancelled) setState({ data, loading: false, error: null }); })
.catch(err => { if (!cancelled) setState({ data: null, loading: false, error: err }); });
return () => { cancelled = true; };
}, [url]);
return state;
}
注意:自己写 useFetch 适合学习,生产请用 TanStack Query / SWR——它们处理缓存、重试、并发、SSR hydration、focus refetch……一堆你写不周到的事。
一个 hook 应该做一件事
❌ 全能 hook:
function useUserAndCartAndTheme() { ... }
✅ 拆分:
useUser(); useCart(); useTheme();
各自独立、各自可测试、各自可选用。
hook 之间组合
function useDebouncedSearch(initialQ: string) {
const [q, setQ] = useState(initialQ);
const debouncedQ = useDebounce(q, 300);
const { data } = useFetch(`/api/search?q=${debouncedQ}`);
return { q, setQ, results: data };
}
Hook 调 hook 是常态——这是 React 复用逻辑的核心方式。
坑
- 不能条件调用:
if (x) useState(...)报错。要把分支放在 hook 内部。 - 依赖数组:自定义 hook 里的 effect 同样要写完整依赖。
- 不要返回响应式对象的"快照":返回
{ value, setValue }而不是value.snapshot()。 - TypeScript 推断:
const [a, b] = useFoo()想要元组而不是数组——用as const或显式类型。
命名建议
useX:返回数据 / 状态useXAction:返回操作(如useLogout())- 不要起名
getX/withX/createX——hook 不是普通函数也不是 HOC
→ 下一篇 Suspense 与流式渲染