一句话规则

任何 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 与流式渲染