从入门到进阶:用例子讲透 React Hooks

November, 2nd 2025 13 min read
React Hooks 全面指南:通俗讲解与实战示例

这不是“百科式列表”,而是“能落地的心智模型”。每个 Hook 给你三样东西:它解决的痛点、何时用、易错点 + 最小示例。

面向小白的阅读指引:

  • 你将学到什么
    • 用“渲染驱动”的思维理解 React,而不是“生命周期函数”的背诵。
    • 明白 Hooks 两条铁律、依赖数组该怎么写、为什么会出现“状态没更新/值不对”的问题。
    • 看得懂最常用的 Hooks,并能在真实页面中组合使用。
  • 前置准备
    • 了解一点点 ES6(解构、箭头函数)即可;不会 TypeScript 也能读懂代码。
    • 安装 React DevTools(浏览器扩展),方便调试组件树与状态。
  • 心智模型(非常关键)
    • 组件是“纯函数 + 状态”。每次渲染都会“重新执行函数”,得到新的 UI。
    • Hooks 是“记忆与副作用的接口”。useState 记住值,useEffect 处理渲染后的副作用。
    • 任何 Hook 都和“渲染”绑定:依赖数组写谁,就代表这个 Hook 对谁“敏感”。

Hooks 两条规则(记住就不易踩坑):

  1. 只在组件最顶层调用 Hooks(不要写在 if、for 里)。
  2. 只在 React 函数组件或自定义 Hook 里调用 Hooks(不要在普通函数里随便用)。

渲染与 effect 的执行顺序(简化版):

  1. 触发一次状态更新(例如点击按钮)。
  2. React 重新执行组件函数 → 计算得到新的 JSX。
  3. 浏览器把变化渲染到页面。
  4. React 调用本次渲染对应的 useEffect 回调;下次渲染前,会先运行上一次的清理函数(如果有)。

这解释了两个常见现象:

  • 在一次渲染的事件处理函数里,读到的“状态值”就是这次渲染时的旧值(闭包特性),直到下一次渲染才会看到新值。
  • useEffect 的回调总是发生在“渲染之后”,所以不要用它去“计算渲染要用的值”,而应该用它去“同步渲染带来的副作用”(如订阅、日志、网络请求等)。

目录

  • 基础状态与副作用:useState / useEffect / useRef
  • 性能与缓存:useMemo / useCallback
  • 复杂状态:useReducer / useContext
  • 并发与响应:useTransition / useDeferredValue
  • 布局与命令式:useLayoutEffect / useImperativeHandle
  • 生态与新能力:useId / useSyncExternalStore / useInsertionEffect / useOptimistic
  • 自定义 Hook:抽象复用的正确姿势
  • 常见误区与调试技巧

基础:useState / useEffect / useRef

痛点与场景:

  • useState:组件内部可响应的状态。
  • useEffect:对“渲染结果”做副作用(订阅、请求、DOM 接入等)。
  • useRef:跨渲染保存可变值(不触发重渲染)或拿到 DOM。

示例:计数器 + 本地存储持久化

tsx
123456789101112131415161718
      import { useEffect, useRef, useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(() => Number(localStorage.getItem('count') || 0));
  const renders = useRef(0);
  renders.current += 1;

  useEffect(() => {
    localStorage.setItem('count', String(count));
  }, [count]);

  return (
    <div>
      <p>Count: {count} (renders: {renders.current})</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}
    

易错点:

  • 不要在 effect 内直接更新“它自己的依赖状态”导致死循环;必要时用条件判断或将副作用与状态更新解耦。
  • useRef 变化不会触发重渲染;适合保存“最新值”或外部实例。

性能:useMemo / useCallback

痛点:避免重复计算、稳定函数引用(传给子组件或依赖 hooks)。

示例:昂贵计算缓存与回调稳定

tsx
123456789101112131415161718192021222324
      import { memo, useCallback, useMemo, useState } from 'react';

const Expensive = memo(function Expensive({ n, onPick }: { n: number; onPick: (v: number) => void }) {
  const value = useMemo(() => {
    // 模拟昂贵计算
    let s = 0;
    for (let i = 0; i < 5_000_00; i++) s += i % 10;
    return s + n;
  }, [n]);
  return <div onClick={() => onPick(value)}>Value: {value}</div>;
});

export default function App() {
  const [n, setN] = useState(1);
  const [picked, setPicked] = useState<number | null>(null);
  const onPick = useCallback((v: number) => setPicked(v), []);
  return (
    <div>
      <input type="number" value={n} onChange={e => setN(Number(e.target.value))} />
      <Expensive n={n} onPick={onPick} />
      <p>picked: {picked}</p>
    </div>
  );
}
    

要点:

  • useMemo/Callback 是“性能优化工具”,不是为了解决依赖警告;不要到处乱加。
  • 只有当子组件依赖引用稳定性(memo/依赖数组)或计算开销大时再用。

复杂状态:useReducer / useContext

痛点:多个字段/复杂变更逻辑,或跨组件共享状态。

示例:Todo with reducer + context(就近状态 + 受控更新)

tsx
123456789101112131415161718192021222324
      import { createContext, useContext, useReducer } from 'react';

type Todo = { id: number; text: string; done: boolean };
type Action = { type: 'add'; text: string } | { type: 'toggle'; id: number };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'add': return [...state, { id: Date.now(), text: action.text, done: false }];
    case 'toggle': return state.map(t => t.id === action.id ? { ...t, done: !t.done } : t);
  }
}

const TodosCtx = createContext<{ state: Todo[]; dispatch: React.Dispatch<Action> } | null>(null);

export function TodosProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, []);
  return <TodosCtx.Provider value={{ state, dispatch }}>{children}</TodosCtx.Provider>;
}

export function useTodos() {
  const ctx = useContext(TodosCtx);
  if (!ctx) throw new Error('useTodos must be used within TodosProvider');
  return ctx;
}
    

使用:

tsx
1234567891011121314151617181920
      function Add() {
  const { dispatch } = useTodos();
  return <button onClick={() => dispatch({ type: 'add', text: 'learn hooks' })}>Add</button>;
}

function List() {
  const { state, dispatch } = useTodos();
  return (
    <ul>
      {state.map(t => (
        <li key={t.id}>
          <label>
            <input type="checkbox" checked={t.done} onChange={() => dispatch({ type: 'toggle', id: t.id })} />
            {t.text}
          </label>
        </li>
      ))}
    </ul>
  );
}
    

并发体验:useTransition / useDeferredValue

痛点:重计算或列表过滤卡顿;希望先让输入响应,慢任务稍后再做。

示例:搜索输入丝滑,列表慢慢算

tsx
123456789101112131415161718192021
      import { useDeferredValue, useMemo, useState, useTransition } from 'react';

export default function Search() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();
  const deferred = useDeferredValue(query);

  const list = useMemo(() => {
    // 模拟大列表过滤
    const data = Array.from({ length: 20000 }, (_, i) => `item-${i}`);
    return data.filter(x => x.includes(deferred));
  }, [deferred]);

  return (
    <div>
      <input value={query} onChange={e => startTransition(() => setQuery(e.target.value))} />
      {isPending && <span>Updating…</span>}
      <p>Found: {list.length}</p>
    </div>
  );
}
    

要点:

  • useTransition 让“状态更新”变成低优先级;useDeferredValue 让“值本身”延后参与计算。

布局与命令式:useLayoutEffect / useImperativeHandle

痛点:需要在浏览器绘制前读/写布局;或对外暴露有限的命令式 API。

示例:自动滚动 + 暴露滚动到顶方法

tsx
123456789101112131415161718
      import { forwardRef, useImperativeHandle, useLayoutEffect, useRef } from 'react';

export type ListRef = { scrollToTop: () => void };

export const List = forwardRef<ListRef, { items: string[] }>(function List({ items }, ref) {
  const el = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const node = el.current;
    if (node) node.scrollTop = node.scrollHeight; // 初次渲染后滚动到底
  }, [items.length]);

  useImperativeHandle(ref, () => ({
    scrollToTop() { el.current?.scrollTo({ top: 0, behavior: 'smooth' }); },
  }), []);

  return <div ref={el} style={{ maxHeight: 200, overflow: 'auto' }}>{items.map(i => <div key={i}>{i}</div>)}</div>;
});
    

生态与新能力:useId / useSyncExternalStore / useInsertionEffect / useOptimistic

  • useId:SSR/CSR 一致的稳定 ID(表单/无障碍标签)。
  • useSyncExternalStore:从外部 store 读取状态(如 Redux、Zustand 底层适配)。
  • useInsertionEffect:在样式注入前同步执行(CSS-in-JS 库使用)。
  • useOptimistic:对异步操作先行“乐观更新”,失败再回滚(表单提交等)。

示例:useId 关联 label/input

tsx
1234567891011
      import { useId } from 'react';

export function EmailField() {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>Email</label>
      <input id={id} type="email" />
    </div>
  );
}
    

示例:useOptimistic(乐观 UI)

tsx
12345678910111213141516171819202122232425
      import { useOptimistic, useState } from 'react';

async function save(name: string) {
  await new Promise(r => setTimeout(r, 800));
  return name.toUpperCase();
}

export default function Profile() {
  const [name, setName] = useState('tom');
  const [optimisticName, setOptimisticName] = useOptimistic(name);

  async function submit() {
    setOptimisticName(name + '...'); // 立即反馈
    const saved = await save(name);
    setName(saved);
  }

  return (
    <div>
      <input value={name} onChange={e => setName(e.target.value)} />
      <button onClick={submit}>Save</button>
      <p>Preview: {optimisticName}</p>
    </div>
  );
}
    

自定义 Hook:抽象复用的正确姿势

两条铁律:

  • 输入输出要稳定(参数、返回值类型清晰),隐藏实现细节。
  • 用“第二次复用”作为抽象信号,不要过早通用化。

示例:useFetch(最小实现,演示取消与状态机)

tsx
1234567891011121314151617181920212223
      import { useEffect, useRef, useState } from 'react';

type State<T> = { data?: T; error?: Error; loading: boolean };

export function useFetch<T>(url: string, init?: RequestInit) {
  const [state, setState] = useState<State<T>>({ loading: true });
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => {
    const ac = new AbortController();
    abortRef.current = ac;
    setState({ loading: true });
    fetch(url, { ...init, signal: ac.signal })
      .then(r => r.json())
      .then((data: T) => setState({ data, loading: false }))
      .catch(e => {
        if (e.name !== 'AbortError') setState({ error: e, loading: false });
      });
    return () => ac.abort();
  }, [url]);

  return state;
}
    

常见误区与调试技巧

  • 依赖数组怎么写:不是“想依赖谁就依赖谁”,而是“你的回调/效果闭包里读了谁就依赖谁”。用 ESLint 的 react-hooks/exhaustive-deps 辅助检查。
  • 只执行一次的 effect:多数用于“订阅/注册/初始化”。要返回清理函数,避免内存泄漏:return () => unsubscribe()
  • 闭包与“读到旧值”:同一轮渲染内,事件处理函数捕获的是当时的值。要获取最新值可用 useRef 保存,或用函数式更新:setCount(c => c + 1)
  • 何时不用 useEffect:派生数据请用 useMemo;由一次交互直接触发的逻辑放在“事件处理函数”里,不要放 effect 里。
  • 受控表单:表单值存到 useState,用 value/onChange 控制;避免受控/非受控混用产生警告。
  • 批量更新:React 会批处理同一 tick 内的多次 setState;依赖前一个值时用函数式更新,防止丢更新。
  • DevTools 调试:组件树中选中节点可查看 Hooks 状态;Profiler 面板能看渲染耗时,定位不必要的重新渲染。
  • Strict Mode:开发环境可能触发“双执行”以帮助发现副作用问题;不要据此判断生产行为。

新手速查 FAQ(遇到就翻)

  • setState 为什么没立刻更新?
    • 更新是“异步、批量”的;本次渲染中读到的是旧值。需要基于旧值更新时用函数式写法:setX(x => x + 1)
  • useEffect 依赖该怎么写?
    • 让 ESLint 插件告诉你。回调里读了谁,就把谁放进依赖;若不想触发,通常是设计要调整,而非删依赖。
  • useEffect 和 useMemo 有啥区别?
    • useMemo 是“渲染期的纯计算缓存”;useEffect 是“渲染后做副作用”。不要混用。
  • 请求该放哪里?
    • 客户端组件:可以放 useEffect,注意竞态取消;Next App Router 推荐服务端数据获取优先。
  • 我需要全局状态吗?
    • 优先用组件就近状态;跨层传递再考虑 Context;更复杂再看外部状态库(Zustand/Redux)。

小结

  • Hooks 是“渲染驱动”模型的拼装件:状态、派生、效果、并发、边界。
  • 先理解痛点与心智模型,再按需挑选;不要用 Hook 掩盖设计问题。
  • 在真实项目里,配合类型、状态机、数据层抽象,Hooks 会更“稳”。