人人都会AI编程

8.3 进阶 Hooks

更新时间:2026-07-11

当基础 Hooks 无法满足复杂的交互需求时,React 提供了一系列进阶 Hooks,用于处理复杂状态、暴露组件方法、同步布局副作用以及优化并发渲染体验。掌握这些 Hooks 能让你应对更复杂的场景,写出更灵活且高性能的组件。

useReducer:复杂状态管理

useReduceruseState 的替代方案,适用于涉及多个子值的复杂状态逻辑下一状态依赖上一状态的场景。它的设计模式类似于 Redux,将所有状态更新逻辑集中到一个纯函数(Reducer)中。

核心语法:

const [state, dispatch] = useReducer(reducer, initialState);

参数与返回值:

  • reducer(state, action):接收当前状态和动作,返回新状态
  • initialState:初始状态值
  • 返回当前 state 和一个 dispatch 函数,用于派发动作

实际场景:购物车状态管理

// 定义 action 类型
const ACTIONS = {
  ADD_ITEM: 'add_item',
  REMOVE_ITEM: 'remove_item',
  CLEAR: 'clear',
};

function cartReducer(state, action) {
  switch (action.type) {
    case ACTIONS.ADD_ITEM:
      return { ...state, items: [...state.items, action.payload] };
    case ACTIONS.REMOVE_ITEM:
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload.id),
      };
    case ACTIONS.CLEAR:
      return { items: [] };
    default:
      return state;
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, { items: [] });

  const addItem = (product) => {
    dispatch({ type: ACTIONS.ADD_ITEM, payload: product });
  };

  return (
    <div>
      <h2>购物车 ({cart.items.length})</h2>
      <button onClick={() => dispatch({ type: ACTIONS.CLEAR })}>清空</button>
      {cart.items.map(item => <div key={item.id}>{item.name}</div>)}
    </div>
  );
}

优势:

  • 状态逻辑集中、可预测,易于调试。
  • 非常适合状态转换存在多种操作(增、删、改、重置等)的组件。
  • 便于单元测试 reducer 函数。

何时选用 useReducer 而非 useState?

  • 状态是复杂对象或数组,且有多处需要按照不同规则更新。
  • 下一个状态依赖上一个状态,且逻辑较复杂。
  • 你希望将更新逻辑与组件渲染分离,提升可测试性。

useImperativeHandle:限制暴露的实例方法

默认情况下,函数组件不向父组件暴露任何实例。当父组件通过 ref 获取子组件时,只能拿到子组件的 DOM 节点(通过 forwardRef 转发)。useImperativeHandle 允许你自定义暴露给父组件的实例值或方法,从而精确控制外部可调用的接口。

必须配合 forwardRef 使用:

import { forwardRef, useImperativeHandle, useRef } from 'react';

const InputField = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = '';
    },
  }));

  return <input ref={inputRef} type="text" />;
});

function Parent() {
  const childRef = useRef(null);

  return (
    <div>
      <InputField ref={childRef} />
      <button onClick={() => childRef.current.focus()}>聚焦</button>
      <button onClick={() => childRef.current.clear()}>清空</button>
    </div>
  );
}

适用场景:

  • 需要命令式调用子组件内部方法(焦点控制、滚动到特定位置、动画触发等)。
  • 封装第三方库时,暴露有限的外部 API。

最佳实践:

  • 避免过度使用。React 推崇声明式交互,仅在确实需要命令式调用时才使用。
  • 只暴露必要的方法,而非整个子组件内部状态。

useLayoutEffect:同步执行的副作用

useLayoutEffectuseEffect 几乎相同,但会在所有 DOM 变更之后、浏览器绘制之前同步执行。这意味着它能阻塞浏览器的渲染,直到副作用执行完毕。因此,它适用于需要在浏览器绘制前同步读取或修改 DOM 的场景。

执行时机对比:

  • useEffect:浏览器绘制后异步执行,不会阻塞视觉更新。
  • useLayoutEffect:DOM 更新后、浏览器绘制前同步执行。

典型应用:防止闪烁的 DOM 测量与修改

function Tooltip({ text, targetRef }) {
  const tooltipRef = useRef(null);
  const [pos, setPos] = useState({ x: 0, y: 0 });

  useLayoutEffect(() => {
    if (!targetRef.current || !tooltipRef.current) return;
    const targetRect = targetRef.current.getBoundingClientRect();
    const tooltipRect = tooltipRef.current.getBoundingClientRect();
    // 在绘制前计算位置,避免用户看到定位跳变
    setPos({
      x: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2,
      y: targetRect.top - tooltipRect.height - 8,
    });
  }, [targetRef]);

  return (
    <div
      ref={tooltipRef}
      style={{ position: 'fixed', left: pos.x, top: pos.y }}
    >
      {text}
    </div>
  );
}

注意事项:

  • 绝大多数场景应优先使用 useEffect,因为它不会阻塞渲染,性能更好。
  • useLayoutEffect 内的代码会阻止浏览器重绘,过多或耗时操作会导致界面卡顿。
  • 服务端渲染时,useLayoutEffect 不会执行并会显示警告,需注意兼容。

useTransition:标记低优先级更新

useTransition 是 React 18 并发特性之一,用于将某些状态更新标记为低优先级过渡更新。它允许你在等待低优先级更新完成时保持 UI 响应,避免复杂渲染阻塞用户与紧急交互(如输入、点击)。

返回值:

  • isPending:布尔值,表示过渡更新是否仍在执行。
  • startTransition:回调函数,用于包裹低优先级的状态更新。

实际场景:搜索列表的过滤

function SearchPage() {
  const [query, setQuery] = useState('');
  const [filteredItems, setFilteredItems] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleChange = (e) => {
    const value = e.target.value;
    // 紧急更新:输入框值需要立即响应
    setQuery(value);
    // 非紧急更新:列表过滤可以延迟,允许用户继续输入
    startTransition(() => {
      const filtered = allItems.filter(item => item.includes(value));
      setFilteredItems(filtered);
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <span>搜索中…</span>}
      <List items={filteredItems} />
    </div>
  );
}

当用户快速输入时,React 会中断之前的过滤更新,只保留最后一次,从而保持输入流畅。isPending 可以用于显示 loading 指示器,提升感知体验。

使用要点:

  • 仅用于非紧急更新,如列表渲染、图表重绘、大量数据展示。
  • 不要将紧急交互(输入框值变化、按钮点击反馈)放入 startTransition
  • 搭配 SuspenseuseDeferredValue 可进一步增强体验。

useDeferredValue:延迟获取新值

useDeferredValueuseTransition 目的类似,但形式不同。它接收一个值,并返回该值的延迟版本,延迟版本会在紧急更新完成后再更新。它适用于你无法直接控制状态设置,但希望推迟某个值的更新的情况。

典型场景:当 props 变化导致开销昂贵的重渲染时

function ProductList({ searchText }) {
  // 延迟 searchText 的更新,避免快速输入时频繁渲染过滤列表
  const deferredSearch = useDeferredValue(searchText);
  // 使用 deferredSearch 进行过滤和渲染
  const filteredProducts = useMemo(
    () => allProducts.filter(p => p.name.includes(deferredSearch)),
    [deferredSearch]
  );

  return (
    <div>
      {filteredProducts.map(p => <ProductCard key={p.id} {...p} />)}
    </div>
  );
}

当父组件传入的 searchText 快速变化时,deferredSearch 会滞后于原始值更新,React 会在空闲时处理过滤渲染,不让输入卡顿。你可以通过比较 searchText !== deferredSearch 判断内容是否陈旧,以显示 loading。

与 useTransition 的对比:

  • useTransition:由你主动包裹状态更新来标记低优先级。
  • useDeferredValue:被动地“延迟”一个外部传入的值,适用于状态更新不在当前组件的情况。

两者都基于并发特性,能让你的应用在大量计算下保持交互流畅。


这些进阶 Hooks 共同构成了 React 应对复杂场景的工具箱。在实际开发中,应先考虑基础 Hooks 是否够用,只有在确实需要控制渲染时机、暴露命令式方法或优化并发交互时才引入它们,避免过度设计和代码复杂化。