首页 / React 19 入门教程 / Hook 规则与进阶

React 19 入门教程

Hook 规则与进阶

本教程共 50 篇 · 第 34 篇 · 更新于 2026-07-29 · 约 7 分钟阅读

ReactHook规则ESLintuseIduseLayoutEffect

34. Hook 规则与进阶

本节目标:掌握 Hook 的两条核心规则,了解原理和进阶用法,写出符合规范的 Hook 代码。

规则一:只在顶层调用

不在循环、条件、嵌套函数里调用 Hook:

// 错误:条件里调用 Hook
function Form() {
  if (isLoggedIn) {
    const [user, setUser] = useState(null); // 不行!
  }
}

// 错误:循环里调用 Hook
function List({ items }) {
  items.forEach(item => {
    const [expanded, setExpanded] = useState(false); // 不行!
  });
}

// 正确:Hook 在组件顶层
function Form() {
  const [user, setUser] = useState(null);
  if (isLoggedIn) { /* ... */ }
}

为什么不能在条件里调用

React 靠 Hook 的调用顺序来关联状态。条件导致顺序变化,状态就乱套了。

规则二:只在函数组件或自定义 Hook 里调用

// 错误:普通函数里调用
function fetchData() {
  const [data, setData] = useState(null); // 不行!
}

// 错误:类组件里调用
class MyComponent extends React.Component {
  render() {
    const [count, setCount] = useState(0); // 不行!
  }
}

// 正确:函数组件
function MyComponent() {
  const [count, setCount] = useState(0);
}

// 正确:自定义 Hook
function useMyHook() {
  const [count, setCount] = useState(0);
}

用 ESLint 插件自动检查

安装 eslint-plugin-react-hooks,它会自动检测违规:

npm install eslint-plugin-react-hooks --save-dev
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';

export default [
  plugins: {
    'react-hooks': reactHooks,
  },
  rules: {
    'react-hooks/rules-of-hooks': 'error',       // 检查规则
    'react-hooks/exhaustive-deps': 'warn',       // 检查 effect 依赖
  },
];

别忽略 exhaustive-deps 警告

这个规则会检查 useEffect 的依赖是否完整。偶尔可以抑制,但要确认自己在做什么。

Hook 的原理(简化版)

React 内部用一个链表存储 Hook 状态。每次渲染,React 按顺序遍历链表:

第一次渲染:
useState(0)  → 节点1: { state: 0 }
useEffect(...) → 节点2: { effect: fn }

第二次渲染:
useState(0)  → 节点1: { state: 0 }  // 按顺序匹配
useEffect(...) → 节点2: { effect: fn }

如果第二次渲染时,第一个 useState 前面插了个 useState,链表就对不上了。这就是为什么不能在条件里调用 Hook。

useId:生成唯一 ID

useId 生成稳定唯一的 ID,适合表单 label 关联:

import { useId } from 'react';

function Form() {
  const id = useId();

  return (
    <>
      <label htmlFor={id}>用户名</label>
      <input id={id} />
    </>
  );
}

多个组件使用也不会冲突:

function App() {
  return (
    <>
      <Form />  // id: ":r0:"
      <Form />  // id: ":r1:"
    </>
  );
}

useId 的 ID 格式

生成的 ID 形如 :r0::r1:,带冒号。不能用在 CSS 选择器里。

useLayoutEffect:浏览器绘制前执行

和 useEffect 类似,但在浏览器绘制前同步执行。适合需要读取 DOM 布局的场景:

import { useLayoutEffect, useRef, useState } from 'react';

function Tooltip() {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height); // 在绘制前更新,避免闪烁
  }, []);

  return <div ref={ref}>...</div>;
}

useLayoutEffect 会阻塞渲染

大多数情况用 useEffect。只有”读取布局 → 立即更新 → 避免闪烁”时才用 useLayoutEffect。

useImperativeHandle:暴露组件方法

配合 forwardRef,让父组件调用子组件的方法:

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

const FancyInput = forwardRef(function FancyInput(props, ref) {
  const inputRef = useRef(null);

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

  return <input ref={inputRef} {...props} />;
});

// 父组件
function Parent() {
  const ref = useRef(null);

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

useDebugValue:自定义调试标签

在 React DevTools 里显示自定义标签:

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  // ...
  useDebugValue(isOnline ? '在线' : '离线');
  return isOnline;
}

DevTools 里会显示 useOnlineStatus: 在线,方便调试。

Hook 最佳实践

  1. 自定义 Hook 抽重复逻辑:两个组件有相同逻辑,提取成 Hook
  2. Hook 保持简单:一个 Hook 只做一件事
  3. 命名清晰useUserPostsuseData
  4. 返回值一致:数组或对象,选一种风格坚持
  5. 文档注释:复杂 Hook 加 JSDoc 说明
/**
 * 订阅在线状态
 * @returns {boolean} 当前是否在线
 */
function useOnlineStatus() {
  // ...
}

常见错误排查

错误原因解决
”Rendered more hooks than previous render”条件/循环里调 Hook移到顶层
”Invalid hook call”在普通函数/类组件里调 Hook只在函数组件调
effect 无限循环依赖数组缺项或对象依赖检查依赖
状态不同步闭包捕获旧值用函数式更新

本节要点

  • 只在顶层调用 Hook,不在条件/循环里调用
  • 只在函数组件或自定义 Hook 里调用
  • ESLint 插件自动检查
  • useId 生成唯一 ID,useLayoutEffect 在绘制前执行
  • 自定义 Hook 是逻辑复用的最佳方式