Skip to content

深入理解 React Hooks 完整体系

所有 19 个内置 Hook 完整 API、底层原理、自定义 Hook 设计模式与实战集合

什么是 React Hooks?

定义:Hooks 是 React 16.8 引入的函数式 API,让函数组件拥有状态管理、副作用处理、上下文访问等能力。React 19 新增了 useActionStateuseOptimisticuse 三个 Hook,总计 19 个内置 Hook。

涉及场景

  • 状态管理:组件内部状态(useState)、复杂状态逻辑(useReducer)
  • 副作用处理:数据获取、DOM 操作、订阅(useEffect / useLayoutEffect)
  • 性能优化:缓存计算结果和函数引用(useMemo / useCallback)
  • 并发交互:非紧急更新(useTransition)、延迟值(useDeferredValue)
  • 表单处理:Action 状态管理(useActionState)、乐观更新(useOptimistic)
  • 上下文共享:跨组件通信(useContext / use)

作用

  1. 取代类组件:函数组件 + Hooks 成为 React 开发的唯一范式
  2. 逻辑复用:自定义 Hook 替代 HOC 和 render props,更简洁、更可组合
  3. 关注点分离:按功能组织代码,而非按生命周期分散逻辑
  4. 面试核心:Hooks 原理(链表、闭包)和正确使用是中高级面试必考

一、状态类 Hooks

useState

jsx
// 基本用法
const [count, setCount] = useState(0);

// 惰性初始化(只在首次渲染执行)
const [data, setData] = useState(() => {
  return JSON.parse(localStorage.getItem('data')) || [];
});

// 函数式更新(基于前一个状态)
setCount(prev => prev + 1);

// 对象状态(必须返回新引用)
const [form, setForm] = useState({ name: '', email: '' });
setForm(prev => ({ ...prev, name: '张三' }));

底层原理

首次渲染:
  创建 Hook 节点 → { memoizedState: initialState, queue: updateQueue, next: null }
  挂载到 Fiber 节点的 memoizedState 链表上

后续渲染:
  按顺序从链表取出对应 Hook 节点
  处理 queue 中的所有 update(批量合并)
  用 Object.is 对比新旧值,相同则 bailout(跳过渲染)

注意事项

  • 更新是异步批量的(React 18+ 所有场景自动批量)
  • Object.is(oldState, newState) 为 true 时跳过更新
  • 不要直接修改对象/数组,必须创建新引用

useReducer

jsx
type State = { count: number; step: number };
type Action =
  | { type: 'increment' }
  | { type: 'decrement' }
  | { type: 'setStep'; payload: number }
  | { type: 'reset' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { ...state, count: state.count + state.step };
    case 'decrement': return { ...state, count: state.count - state.step };
    case 'setStep': return { ...state, step: action.payload };
    case 'reset': return { count: 0, step: 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
  // 惰性初始化
  // const [state, dispatch] = useReducer(reducer, initialArg, init);

  return (
    <div>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'increment' })}>+{state.step}</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-{state.step}</button>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: 'setStep', payload: Number(e.target.value) })}
      />
    </div>
  );
}

vs useState

  • dispatch 的引用永远稳定,传给子组件不需要 useCallback
  • 多个状态相互关联时,reducer 让状态转换更可预测
  • 适合配合 Context 实现简易全局状态管理

useSyncExternalStore

订阅外部 store(非 React 状态源),保证并发模式下的数据一致性。

jsx
import { useSyncExternalStore } from 'react';

// 订阅浏览器在线状态
function useOnlineStatus() {
  return useSyncExternalStore(
    // subscribe:订阅函数,返回取消订阅的函数
    (callback) => {
      window.addEventListener('online', callback);
      window.addEventListener('offline', callback);
      return () => {
        window.removeEventListener('online', callback);
        window.removeEventListener('offline', callback);
      };
    },
    // getSnapshot:获取当前状态(客户端)
    () => navigator.onLine,
    // getServerSnapshot:SSR 时的状态(可选)
    () => true
  );
}

// 订阅自定义 store
const store = {
  state: { count: 0 },
  listeners: new Set(),
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  },
  getSnapshot() {
    return store.state;
  },
  increment() {
    store.state = { ...store.state, count: store.state.count + 1 };
    store.listeners.forEach(l => l());
  },
};

function Counter() {
  const { count } = useSyncExternalStore(store.subscribe, store.getSnapshot);
  return <button onClick={() => store.increment()}>{count}</button>;
}

使用场景:第三方状态管理库(Zustand/Redux 底层都用了它)、浏览器 API 订阅、WebSocket 消息。


二、副作用类 Hooks

useEffect

jsx
// 1. 基本:每次渲染后执行
useEffect(() => {
  document.title = `计数: ${count}`;
});

// 2. 依赖数组:仅在 userId 变化时执行
useEffect(() => {
  let cancelled = false;
  fetchUser(userId).then(user => {
    if (!cancelled) setUser(user);
  });
  return () => { cancelled = true; }; // 清理
}, [userId]);

// 3. 空数组:仅挂载时执行一次
useEffect(() => {
  const ws = new WebSocket('wss://api.example.com');
  ws.onmessage = (e) => handleMessage(JSON.parse(e.data));
  return () => ws.close(); // 卸载时关闭
}, []);

执行时机

渲染 → DOM 更新 → 浏览器绘制 → useEffect 执行(异步)

                              不阻塞浏览器渲染

清理函数的调用时机

  1. 下次 effect 执行之前
  2. 组件卸载

useLayoutEffect

useEffect API 完全相同,但在 DOM 更新后、浏览器绘制前同步执行。

jsx
// 用途:需要在绘制前读取/修改 DOM
function Tooltip({ anchorRef, children }) {
  const tooltipRef = useRef(null);
  const [position, setPosition] = useState({ top: 0, left: 0 });

  useLayoutEffect(() => {
    // 在浏览器绘制前计算位置,避免闪烁
    const anchor = anchorRef.current.getBoundingClientRect();
    const tooltip = tooltipRef.current.getBoundingClientRect();
    setPosition({
      top: anchor.top - tooltip.height - 8,
      left: anchor.left + (anchor.width - tooltip.width) / 2,
    });
  }, [anchorRef]);

  return (
    <div ref={tooltipRef} style={{ position: 'fixed', ...position }}>
      {children}
    </div>
  );
}

执行时机

渲染 → DOM 更新 → useLayoutEffect 执行(同步)→ 浏览器绘制

                  阻塞绘制!避免滥用

选择规则:默认用 useEffect;只在需要同步读取 DOM 布局防止视觉闪烁时才用 useLayoutEffect

useInsertionEffect

React 18 新增,在 DOM 变更之前执行。专为 CSS-in-JS 库设计(如 styled-components、Emotion)。

jsx
// 仅供 CSS-in-JS 库作者使用,应用开发者几乎不需要
useInsertionEffect(() => {
  const style = document.createElement('style');
  style.textContent = `.my-class { color: red; }`;
  document.head.appendChild(style);
  return () => document.head.removeChild(style);
}, []);

执行顺序useInsertionEffect → DOM 变更 → useLayoutEffect → 绘制 → useEffect


三、引用类 Hooks

useRef

jsx
// 1. DOM 引用
function TextInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>聚焦</button>
    </>
  );
}

// 2. 可变值容器(不触发渲染)
function Timer() {
  const intervalRef = useRef<number | null>(null);
  const countRef = useRef(0);

  const start = () => {
    intervalRef.current = window.setInterval(() => {
      countRef.current += 1;
      console.log(countRef.current);
    }, 1000);
  };

  const stop = () => {
    if (intervalRef.current) clearInterval(intervalRef.current);
  };

  useEffect(() => stop, []); // 卸载时清理
  return <button onClick={start}>开始</button>;
}

// 3. 保存前一次渲染的值
function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  useEffect(() => { ref.current = value; });
  return ref.current;
}

useImperativeHandle

自定义通过 ref 暴露给父组件的实例方法。

jsx
// React 19:ref 直接作为 prop
function VideoPlayer({ ref }) {
  const videoRef = useRef<HTMLVideoElement>(null);

  useImperativeHandle(ref, () => ({
    play() { videoRef.current?.play(); },
    pause() { videoRef.current?.pause(); },
    seekTo(time: number) {
      if (videoRef.current) videoRef.current.currentTime = time;
    },
  }), []);

  return <video ref={videoRef} />;
}

// 父组件
function App() {
  const playerRef = useRef(null);
  return (
    <>
      <VideoPlayer ref={playerRef} />
      <button onClick={() => playerRef.current?.play()}>播放</button>
      <button onClick={() => playerRef.current?.seekTo(30)}>跳到30秒</button>
    </>
  );
}

四、上下文类 Hooks

useContext

jsx
const ThemeContext = createContext<'light' | 'dark'>('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>按钮</button>;
}

// Provider
<ThemeContext.Provider value="dark">
  <ThemedButton />
</ThemeContext.Provider>

use(React 19)

use 可在条件语句中调用,读取 Context 或 Promise。

jsx
import { use } from 'react';

// 读取 Context(可在条件中)
function Panel({ showTheme }) {
  if (showTheme) {
    const theme = use(ThemeContext);
    return <div className={theme}>主题面板</div>;
  }
  return <div>默认面板</div>;
}

// 读取 Promise(配合 Suspense)
function UserName({ userPromise }) {
  const user = use(userPromise);
  return <span>{user.name}</span>;
}

<Suspense fallback={<Spinner />}>
  <UserName userPromise={fetchUser(id)} />
</Suspense>

五、性能优化类 Hooks

useMemo

jsx
// 缓存计算结果
const sortedList = useMemo(() => {
  return items
    .filter(item => item.category === category)
    .sort((a, b) => a.name.localeCompare(b.name));
}, [items, category]);

// 缓存 JSX(避免子树重渲染)
const chart = useMemo(() => <ExpensiveChart data={data} />, [data]);

useCallback

jsx
// 缓存函数引用
const handleSubmit = useCallback(async (formData: FormData) => {
  await submitForm(formData);
  refreshList();
}, [refreshList]);

// 本质等价于
const handleSubmit = useMemo(() => async (formData: FormData) => {
  await submitForm(formData);
  refreshList();
}, [refreshList]);

React Compiler 时代:这两个 Hook 将被编译器自动插入,开发者不再需要手动编写。


六、并发类 Hooks

useTransition

jsx
function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab: string) {
    startTransition(() => {
      setTab(nextTab); // 非紧急更新,可被中断
    });
  }

  return (
    <div>
      <nav>
        {['home', 'about', 'posts'].map(t => (
          <button key={t} onClick={() => selectTab(t)}
            style={{ fontWeight: tab === t ? 'bold' : 'normal' }}>
            {t}
          </button>
        ))}
      </nav>
      {isPending && <Spinner />}
      <TabContent tab={tab} />
    </div>
  );
}

useDeferredValue

jsx
function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.2s' }}>
      <SlowList query={deferredQuery} />
    </div>
  );
}

useId

生成在 SSR 和 CSR 之间稳定的唯一 ID,避免水合不匹配。

jsx
function FormField({ label }) {
  const id = useId();
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  );
}

// 一个组件内多个 ID
function PasswordField() {
  const id = useId();
  return (
    <>
      <label htmlFor={`${id}-password`}>密码</label>
      <input id={`${id}-password`} type="password" />
      <label htmlFor={`${id}-confirm`}>确认密码</label>
      <input id={`${id}-confirm`} type="password" />
    </>
  );
}

七、React 19 新增 Hooks

useActionState

jsx
import { useActionState } from 'react';

function AddToCartForm({ productId }) {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const quantity = Number(formData.get('quantity'));
      try {
        await addToCart(productId, quantity);
        return { success: true, message: `已添加 ${quantity} 件`, error: null };
      } catch (err) {
        return { ...prevState, error: err.message };
      }
    },
    { success: false, message: '', error: null }
  );

  return (
    <form action={formAction}>
      <input name="quantity" type="number" defaultValue={1} min={1} />
      <button disabled={isPending}>{isPending ? '添加中...' : '加入购物车'}</button>
      {state.success && <p className="success">{state.message}</p>}
      {state.error && <p className="error">{state.error}</p>}
    </form>
  );
}

useOptimistic

jsx
import { useOptimistic } from 'react';

function MessageList({ messages, sendMessage }) {
  const [optimisticMessages, addOptimistic] = useOptimistic(
    messages,
    (current, newMsg) => [...current, { ...newMsg, sending: true }]
  );

  async function handleSend(formData) {
    const text = formData.get('text');
    const msg = { id: crypto.randomUUID(), text, author: 'me' };
    addOptimistic(msg);
    await sendMessage(msg);
  }

  return (
    <div>
      {optimisticMessages.map(msg => (
        <div key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
          <strong>{msg.author}:</strong> {msg.text}
        </div>
      ))}
      <form action={handleSend}>
        <input name="text" />
        <button>发送</button>
      </form>
    </div>
  );
}

八、调试类 Hook

useDebugValue

在 React DevTools 中为自定义 Hook 显示标签。

jsx
function useOnlineStatus() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  useDebugValue(isOnline ? '🟢 在线' : '🔴 离线');
  return isOnline;
}

// 带格式化(延迟计算,只在 DevTools 打开时执行)
function useFormattedDate(date: Date) {
  useDebugValue(date, d => d.toLocaleDateString('zh-CN'));
  // ...
}

九、Hooks 底层原理

链表结构

Fiber 节点
  └── memoizedState → Hook1 → Hook2 → Hook3 → null
                       ↓        ↓        ↓
                    useState  useEffect  useRef

每个 Hook 节点:
{
  memoizedState: any,    // 当前状态值
  baseState: any,        // 基础状态(用于并发更新合并)
  baseQueue: Update[],   // 待处理的更新
  queue: UpdateQueue,    // 更新队列
  next: Hook | null      // 指向下一个 Hook
}

为什么不能在条件中调用 Hook

jsx
// ❌ 条件调用导致链表错位
function Bad({ show }) {
  if (show) {
    useState('A'); // 第1个 Hook(有时存在,有时不存在)
  }
  useState('B');   // 第2个 Hook(与链表位置不匹配)
}

// 渲染1(show=true): Hook链表 = [A] → [B]
// 渲染2(show=false): Hook链表 = [B](但 React 认为第1个是 A)
// → 状态错乱!

闭包与 Stale Closure

jsx
function Counter() {
  const [count, setCount] = useState(0);

  // 每次渲染创建新的闭包,捕获当时的 count 值
  const handleClick = () => {
    // 这里的 count 是「这次渲染时」的值
    setTimeout(() => {
      console.log(count); // 闭包中的旧值
    }, 3000);
  };

  // 函数式更新不受闭包影响
  const increment = () => setCount(c => c + 1); // c 始终是最新值
}

Mount vs Update 的双重实现

React 内部维护两套 Hook 实现:

Mount 阶段(首次渲染):
  HooksDispatcherOnMount = {
    useState: mountState,      // 创建 Hook 节点,初始化状态
    useEffect: mountEffect,    // 创建 Hook 节点,标记 effect
    useRef: mountRef,          // 创建 { current: initialValue }
  }

Update 阶段(后续渲染):
  HooksDispatcherOnUpdate = {
    useState: updateState,     // 从链表取出 Hook,处理更新队列
    useEffect: updateEffect,   // 对比依赖数组,决定是否重新执行
    useRef: updateRef,         // 直接返回已有的 ref 对象
  }

十、自定义 Hook 实战集合

useLocalStorage

jsx
function useLocalStorage<T>(key: string, initialValue: T) {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback((value: T | ((prev: T) => T)) => {
    setStoredValue(prev => {
      const newValue = value instanceof Function ? value(prev) : value;
      localStorage.setItem(key, JSON.stringify(newValue));
      return newValue;
    });
  }, [key]);

  return [storedValue, setValue] as const;
}

// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');

useDebounce

jsx
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 使用
function Search() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) fetchResults(debouncedQuery);
  }, [debouncedQuery]);
}

useMediaQuery

jsx
function useMediaQuery(query: string): boolean {
  return useSyncExternalStore(
    (callback) => {
      const mql = window.matchMedia(query);
      mql.addEventListener('change', callback);
      return () => mql.removeEventListener('change', callback);
    },
    () => window.matchMedia(query).matches,
    () => false // SSR 默认值
  );
}

// 使用
const isMobile = useMediaQuery('(max-width: 768px)');
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');

useIntersectionObserver

jsx
function useIntersectionObserver(
  options?: IntersectionObserverInit
): [React.RefCallback<Element>, IntersectionObserverEntry | null] {
  const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
  const observerRef = useRef<IntersectionObserver | null>(null);

  const ref = useCallback((node: Element | null) => {
    if (observerRef.current) observerRef.current.disconnect();
    if (!node) return;

    observerRef.current = new IntersectionObserver(([entry]) => {
      setEntry(entry);
    }, options);

    observerRef.current.observe(node);
  }, [options?.threshold, options?.root, options?.rootMargin]);

  return [ref, entry];
}

// 使用:懒加载图片
function LazyImage({ src, alt }) {
  const [ref, entry] = useIntersectionObserver({ threshold: 0.1 });
  const isVisible = entry?.isIntersecting;

  return (
    <div ref={ref}>
      {isVisible ? <img src={src} alt={alt} /> : <Skeleton />}
    </div>
  );
}

useEventListener

jsx
function useEventListener<K extends keyof WindowEventMap>(
  eventName: K,
  handler: (event: WindowEventMap[K]) => void,
  element: EventTarget = window
) {
  const savedHandler = useRef(handler);

  useLayoutEffect(() => {
    savedHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const listener = (event: Event) => savedHandler.current(event as WindowEventMap[K]);
    element.addEventListener(eventName, listener);
    return () => element.removeEventListener(eventName, listener);
  }, [eventName, element]);
}

// 使用
useEventListener('keydown', (e) => {
  if (e.key === 'Escape') closeModal();
});

十一、Hooks 完整速查表

Hook用途触发渲染React 版本
useState状态管理16.8
useReducer复杂状态16.8
useEffect副作用(异步)16.8
useLayoutEffect副作用(同步,绘制前)16.8
useInsertionEffectCSS-in-JS 注入18
useRefDOM 引用 / 可变值16.8
useImperativeHandle自定义 ref 暴露16.8
useContext读取上下文16.8
use读取 Promise/Context19
useMemo缓存计算结果16.8
useCallback缓存函数引用16.8
useTransition非紧急更新18
useDeferredValue延迟值18
useId稳定唯一 ID18
useSyncExternalStore订阅外部 store18
useActionStateAction 状态19
useOptimistic乐观更新19
useDebugValueDevTools 标签16.8

面试高频题

Q: useEffect 和 useLayoutEffect 的区别?

  • useEffect:异步执行,在浏览器绘制后,不阻塞渲染
  • useLayoutEffect:同步执行,在 DOM 更新后、浏览器绘制前,会阻塞渲染
  • 默认用 useEffect,需要同步测量/修改 DOM 时才用 useLayoutEffect

Q: 为什么 Hook 不能在条件语句中调用?

React 通过调用顺序(链表索引)匹配 Hook 与状态。条件调用会导致顺序不一致,后续所有 Hook 与错误的状态配对。use() 是唯一的例外(React 19)。

Q: useMemo 和 useCallback 的本质区别?

useCallback(fn, deps) 等价于 useMemo(() => fn, deps)useMemo 缓存计算结果,useCallback 缓存函数本身。React Compiler 会自动处理这些优化。

Q: 如何避免闭包陷阱?

  1. 函数式更新setState(prev => prev + 1)
  2. useRefref.current 始终指向最新值
  3. 正确设置依赖数组:把所有引用的外部变量加入 deps