Skip to content

React Hooks 基础指南

React Hooks 是 React 16.8 引入的一项重大特性,它允许你在不编写类组件的情况下使用状态和其他 React 特性。

Hooks 使用规则

在使用 Hooks 时,必须遵循以下两条核心规则:

  1. 只在最顶层调用 Hooks:不要在循环、条件或嵌套函数中调用 Hook。这确保了 Hooks 在每次渲染时都以相同的顺序被调用。
  2. 只在 React 函数中调用 Hooks:不要在普通的 JavaScript 函数中调用 Hook,而应该在 React 的函数组件或自定义 Hook 中调用。

1. useState:管理状态

useState 是最常用的 Hook,用于在函数组件中添加状态。

基本用法

tsx
import React, { useState } from 'react';

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

  return (
    <div>
      <p>计数值: {count}</p>
      <button onClick={() => setCount(count + 1)}>增加</button>
    </div>
  );
}

进阶:函数式更新

如果你需要根据之前的状态来计算新的状态,请向 setCount 传递一个函数。这可以确保在多次连续更新时,状态能够正确累加。

tsx
const [count, setCount] = useState(0);

// 正确的做法:基于前一个值更新
const handleIncrement = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1); // 结果会是 +2
};

2. useEffect:处理副作用

useEffect 用于处理副作用,如数据获取、订阅或手动修改 DOM。

执行机制

tsx
useEffect(() => {
  // 这里是副作用逻辑(例如:API 调用)
  console.log('组件挂载或依赖项更新');

  return () => {
    // 这里是清理函数(例如:清除定时器)
    console.log('组件卸载或下一次 Effect 执行前');
  };
}, [dependencies]); // 依赖项数组

依赖项对照表

依赖项执行时机
不传依赖项每次组件渲染后都会执行
空数组 []仅在组件挂载(Mount)时执行一次
有值数组 [count]仅在 count 发生变化时执行

3. useRef:访问 DOM 与持久化变量

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。

访问 DOM 元素

tsx
import React, { useRef } from 'react';

function TextInputWithFocusButton() {
  const inputEl = useRef<HTMLInputElement>(null);

  const onButtonClick = () => {
    // 直接操作 DOM
    inputEl.current?.focus();
  };

  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>聚焦输入框</button>
    </>
  );
}

4. useMemo & useCallback:性能优化

这两个 Hooks 用于缓存计算结果或函数,避免不必要的重渲染。

  • useMemo:返回一个缓存的值
  • useCallback:返回一个缓存的函数
tsx
// useMemo 示例:缓存昂贵的计算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// useCallback 示例:防止传递给子组件的函数在重渲染时改变引用
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

5. useLayoutEffect:同步执行

useLayoutEffectuseEffect 结构相同,但它在 所有的 DOM 变更之后、浏览器绘制之前 同步触发。

useEffect vs useLayoutEffect

特性useEffectuseLayoutEffect
执行时机浏览器完成布局和绘制之后(异步)浏览器渲染之前(同步)
渲染影响不阻塞 DOM 渲染,可能出现闪烁阻塞 DOM 渲染,防止闪烁
主要场景大多数副作用(API、订阅)DOM 尺寸测量、同步布局调整

实际案例:防止数值闪烁

如果你在 useEffect 中修改 DOM 位置,用户可能会看到元素先出现在位置 A,然后闪跳到位置 B。使用 useLayoutEffect 可以消除这种视觉不一致。

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

function Tooltip() {
  const [position, setPosition] = useState(0);
  const divRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    // 在浏览器绘制前获取 DOM 尺寸并更新状态
    if (divRef.current) {
      const { width } = divRef.current.getBoundingClientRect();
      setPosition(width / 2);
    }
  }, []); // 仅在挂载时运行

  return <div ref={divRef} style={{ left: position }}>Tooltip</div>;
}

6. useContext:跨组件通信

useContext 让你能够订阅 React 的 Context 而不必嵌套 Consumer 容器。

tsx
const ThemeContext = React.createContext('light');

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

总结:如何选择 Hook?

  1. 基本状态useState
  2. 大多数副作用(请求数据、修改 Document 标题)用 useEffect
  3. 需要操作 DOMuseRef
  4. 性能瓶颈时考虑 useMemouseCallback
  5. 修改 DOM 布局且需要防闪烁useLayoutEffect
  6. 逻辑非常复杂的状态管理可以考虑 useReducer

为什么 Hooks 不能在循环、条件或嵌套函数中调用?

这是 React 的核心渲染机制决定的。React 并不像我们想象中那样通过“变量名”来记住 Hook 的状态,而是通过 调用顺序

1. 底层原理:链表结构

在 React 内部,每个组件实例都有一个索引,用于记录当前正在执行哪个 Hook。所有的 Hooks 状态都被存储在一个简单的链表(Linked List)中。

  • 第一次渲染时,React 按照代码编写的顺序依次创建链表节点。
  • 第二次(及以后)渲染时,React 会按顺序“对号入座”,取出上一次渲染保存的状态。

2. 如果顺序被打乱会发生什么?

假设你将 Hook 放在了条件判断中:

tsx
function MyComponent() {
  const [name, setName] = useState('Alice');

  // ❌ 错误做法:放在条件语句中
  if (name !== '') {
    useEffect(() => {
      console.log('执行副作用');
    }, []);
  }

  const [count, setCount] = useState(0);
  // ...
}

后果分析:

  1. 渲染 1 (name != ''):Hook 顺序为 useState(name) -> useEffect -> useState(count)
  2. 重渲染 (name == ''):条件不满足,useEffect 被跳过。此时 React 依然按照旧顺序去链表里找第二个 Hook。
  3. 结果:React 会把原本属于 useEffect 的数据(或者位次)错误地分配给 useState(count)

这会导致后续所有 Hook 的状态发生偏移,产生难以调试的 Bug。

3. 如何解决?

如果你需要有条件地执行副作用,请将判断逻辑写在 Hook 内部

tsx
useEffect(() => {
  // ✅ 正确做法:在 Hook 内部进行条件判断
  if (name !== '') {
    console.log('执行副作用');
  }
}, [name]);

Hooks 的调用顺序必须在每次渲染中保持绝对一致,这样 React 才能正确地将状态与对应的 Hook 关联。

一个组件中的 hook 会以链表的形式串起来, FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个 Hook。

在更新时,会复用之前的 Hook,如果通过了条件或循环语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks状态和当前 hooks 不一致的问题。