Skip to content
Hooks

Hooks

Hooks 是 React 16.8 引入的函数组件能力扩展。它们让你在不写 class 的情况下使用 state 和其他 React 特性。


Hooks 规则

两条铁律

  1. 只在顶层调用 — 不要在循环、条件或嵌套函数中调用 Hook
  2. 只在 React 函数中调用 — 函数组件或自定义 Hook 中,不要在普通 JS 函数中调用

🔬 深入原理:React 通过调用顺序将 Hook 与对应的 Fiber 节点关联。Hooks 在 Fiber 节点中是一个单向链表(memoizedState)。如果你在条件中调用 Hook,链表顺序会乱,导致状态错位。

为什么要有这些规则

// ❌ 违反规则 1:条件内调用
if (something) {
  const [v, setV] = useState(0);  // 第一个渲染调用了,下次可能跳过
}

// ✅ 正确:条件放在 Hook 内部
const [v, setV] = useState(0);
if (something) {
  // 使用 v
}

内置 Hooks 全景图

类别 Hooks 用途
状态 useState, useReducer 管理组件内部状态
副作用 useEffect, useLayoutEffect, useInsertionEffect 同步到外部系统
引用 useRef, useImperativeHandle 持有可变值、暴露子组件方法
上下文 useContext 消费 Context
性能 useMemo, useCallback 缓存计算结果、缓存函数引用
过渡 useTransition, useDeferredValue 标记非紧急更新
工具 useId, useDebugValue, useSyncExternalStore, useOptimistic ID 生成、调试、外部 store 等
组合 自定义 Hook 提取可复用逻辑

useState

const [state, setState] = useState(initialValue);

初始化函数(惰性初始化)

// ❌ 每次渲染都执行 expensive(),初始值浪费
const [data, setData] = useState(expensiveComputation());

// ✅ 只执行一次
const [data, setData] = useState(() => expensiveComputation());

函数式更新

// ❌ 依赖闭包中的旧值
setCount(count + 1);     // 连续调用可能只加一次

// ✅ 基于最新值
setCount(c => c + 1);    // 安全,三次调用会加三次

🚨 陷阱setState 是异步的。调用后立即读取 state 仍然是旧值。如需在更新后执行操作,用 useEffect


useEffect

useEffect(() => {
  // 副作用逻辑(Setup)
  const subscription = api.subscribe(prodId);

  return () => {
    // 清理函数(Cleanup)
    subscription.unsubscribe();
  };
}, [prodId]);  // 依赖数组

三种使用模式

// 1. 每次渲染后执行(无依赖数组)
useEffect(() => { ... });

// 2. 仅挂载时执行一次(空数组)
useEffect(() => {
  fetchData();
}, []);

// 3. 依赖变化时执行
useEffect(() => {
  fetchProduct(productId);
}, [productId]);

清理函数的执行时机

组件首次渲染 → 执行 Setup
组件重新渲染 → 执行 Cleanup(用旧依赖)→ 执行 Setup(用新依赖)
组件卸载     → 执行 Cleanup

🚨 陷阱:Strict Mode 下,useEffect 的 Setup 会被调用两次(Setup → Cleanup → Setup),这是有意为之,帮助发现缺少清理的问题。

useEffect vs 直接放在组件体中

组件体 useEffect
执行阶段 Render 阶段 Commit 阶段
时机 可被 React 任意调用 DOM 已更新
用途 计算 JSX 副作用

useLayoutEffect

useLayoutEffect(() => {
  // 在浏览器重绘前同步执行
  measureTooltipSize();
}, [content]);

useEffect 的区别:

useLayoutEffect useEffect
执行时机 DOM 变更后,浏览器重绘前 浏览器重绘后
执行方式 同步阻塞 异步
适用场景 测量 DOM、修复视觉闪烁 数据获取、订阅

💡 最佳实践:90% 的情况用 useEffect。只有当你测量 DOM、需要防止闪烁(如 tooltip 定位)时才用 useLayoutEffect


useRef

const ref = useRef(initialValue);
// ref.current 可直接读写,修改不触发重渲染

三种典型用途

// 1. DOM 引用
function InputFocus() {
  const inputRef = useRef(null);
  useEffect(() => { inputRef.current?.focus(); }, []);
  return <input ref={inputRef} />;
}

// 2. 保存可变值(不触发渲染)
function Timer() {
  const intervalRef = useRef(null);

  const start = () => {
    intervalRef.current = setInterval(() => console.log('tick'), 1000);
  };
  const stop = () => {
    clearInterval(intervalRef.current);
  };
}

// 3. 记录上一次的值
function usePrevious(value) {
  const ref = useRef();
  useEffect(() => { ref.current = value; });
  return ref.current;  // 返回的是"旧值"
}

useContext

const ThemeContext = createContext('light');

// Provider
function App() {
  const [theme, setTheme] = useState('dark');
  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}

// Consumer
function Page() {
  const theme = useContext(ThemeContext);
  return <div className={theme}>当前主题{theme}</div>;
}

🚨 陷阱:Context value 变化会导致所有消费该 Context 的组件重渲染,即使它们只用了 value 的一小部分。解决方案:拆分 Context 或使用状态管理库(如 Zustand)。


useMemo & useCallback

// useMemo:缓存计算结果
const sorted = useMemo(() => {
  return data.sort((a, b) => a.name.localeCompare(b.name));
}, [data]);

// useCallback:缓存函数引用
const handleClick = useCallback((id) => {
  setSelected(id);
}, []);

何时使用(抉择指南)

场景 不需要 需要
计算很简单 `< 1ms 复杂计算(filter/map/sort 大数组)
子组件 原生元素 memo(WrappedComponent)
函数作为 Effect 依赖 普通函数 稳定引用避免无限 effect

🚨 陷阱:不要无差别地对所有函数和值使用 useMemo/useCallback。它们自己也有开销(缓存存储、依赖比较)。只有在性能确实出现问题时再用。


useReducer

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return <button onClick={() => dispatch({ type: 'increment' })}>
    {state.count}
  </button>;
}
useState useReducer
简单值/对象 复杂状态逻辑
更新互相独立 更新互相关联
1-3 个状态变量 多个状态变量需协同管理

💡 最佳实践:当一个 setState 依赖另一个 state 的值时,考虑改用 useReducer


useTransition(React 18+)

const [isPending, startTransition] = useTransition();

function handleSearch(input) {
  setInputValue(input);   // 紧急:立刻更新输入框

  startTransition(() => {
    setSearchResults(filter(input));  // 非紧急:可被中断
  });
}

// isPending 为 true 时显示 loading

💡 最佳实践:将大量状态的更新包裹在 startTransition 中,保证用户交互(如输入、点击)不卡顿。


自定义 Hook

// 提取可复用逻辑的标准方式
function useFetch(url) {
  const [data, setData] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetch(url)
      .then(r => r.json())
      .then(d => { if (!cancelled) setData(d); })
      .catch(e => { if (!cancelled) setError(e); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [url]);

  return { data, error, loading };
}

命名约定

  • Hook 名以 use 开头(ESLint 插件依赖此约定做规则检查)
  • 返回值用对象(而非数组),便于按需解构

Hooks 调用链路

组件渲染
  │
  ▼
调用组件函数
  │
  ▼
React 传入当前 Fiber 的 dispatcher
  │
  ▼
useState → resolveDispatcher().useState()
  │
  ▼
从 Fiber.memoizedState 链表读/写对应槽位

🔬 深入原理:React 为每个渲染周期设置一个 dispatcher。这就是为什么在组件外调用 Hook 会报 “Invalid hook call” —— dispatcher 未设置。


🚨 常见陷阱

  1. 在条件/循环中调用 Hook — 破坏链表顺序
  2. useEffect 缺少依赖 — 导致闭包过时,访问到旧的 props/state
  3. useEffect 死循环 — 设置 state 却没正确设置依赖
  4. 忘记清理 — 定时器、订阅、请求未取消
  5. ref 用作依赖ref.current 变化不会触发 effect
  6. 滥用 useMemo/useCallback — 简单场景不需要
  7. 在 useEffect 中调用 setState 无防抖 — 可能导致 render → effect → render 循环