Skip to content

Hooks 基础:state、effect、ref、context

Hooks 是现代 React 的主线。

但真正困难的地方不是“记住 Hook 名字”,而是:

  1. 每个 Hook 解决的到底是哪类问题
  2. 它和渲染过程是什么关系
  3. 为什么 useEffect、闭包旧值、依赖数组总是容易出问题

这篇文章重点讲:

  1. Hooks 为什么会出现
  2. Hooks 的使用规则是什么
  3. useStateuseEffectuseRefuseContext 各自做什么
  4. useEffect 为什么最容易写出问题

1. Hooks 为什么会出现

在函数组件成为主流之后,React 需要一套方式让函数组件也能承载:

  1. 本地状态
  2. 副作用
  3. 生命周期式能力
  4. 逻辑复用

Hooks 就是在解决这件事。

Hooks 让函数组件不只是“接收 props 返回 JSX”,也能管理状态、处理副作用并复用逻辑。


2. Hooks 的两条基本规则

这两条非常重要。

2.1 只在最外层调用 Hook

不要放在:

  1. if
  2. for
  3. 条件分支内部
  4. 普通嵌套函数内部

原因不是“语法不喜欢”,而是 React 需要稳定的 Hook 调用顺序。

2.2 只在 React 函数组件或自定义 Hook 里调用 Hook

普通工具函数里不能随便调用 Hook。

🌟 这两条规则的核心,不是背规范,而是 React 需要根据组件每次渲染时相同的 Hook 顺序,来把状态槽位对应起来。


3. useState 到底在解决什么问题

useState 用来给函数组件增加本地状态。

例如:

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

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

这里最关键的点是:

  1. count 是当前渲染看到的状态值
  2. setCount 不是直接改变量,而是发起一次状态更新
  3. 状态变化后,组件会进入下一轮渲染

3.1 什么时候用函数式更新

当你的新状态依赖旧状态时,更稳妥的写法通常是:

jsx
setCount(prev => prev + 1);

这能避免:

  1. 闭包读到旧值
  2. 多次更新被同一轮旧值覆盖

4. useEffect 到底在解决什么问题

useEffect 处理的是副作用。

副作用可以看成,那些不属于纯渲染计算,但又需要在组件运行过程中做的事情。

常见场景包括:

  1. 发请求
  2. 订阅事件
  3. 手动操作外部系统
  4. 定时器
  5. 日志和埋点

例如:

jsx
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function loadUser() {
      const data = await fetch(`/api/users/${userId}`).then(res => res.json());
      if (!cancelled) {
        setUser(data);
      }
    }

    loadUser();

    return () => {
      cancelled = true;
    };
  }, [userId]);

  if (!user) {
    return <div>loading...</div>;
  }

  return <div>{user.name}</div>;
}

这段代码最值得注意的是:

  1. effect 依赖了 userId
  2. 依赖变化时会重新执行
  3. 返回清理函数,避免旧请求结果继续污染新状态

5. 依赖数组到底怎么理解

依赖数组不是“随便填让 ESLint 不报错”的东西。

它真正表达的是,这个 effect 依赖哪些渲染时值,只要这些值变化,就应该重新同步副作用。

常见情况:

写法含义
不传依赖数组每次渲染后都执行
[]通常表示挂载后执行一次
[a, b]ab 变化时重新执行

🌟 依赖数组不是“控制 effect 什么时候跑”的开关,而是“声明 effect 依赖了哪些值”的清单。


6. 为什么 useEffect 最容易出问题

因为它天然跨越了两套世界:

  1. React 渲染世界
  2. 外部副作用世界

最常见的问题通常是:

  1. 依赖没写全
  2. 清理没写好
  3. 把太多逻辑塞进一个 effect
  4. 读到了旧闭包里的值

所以更自然的写法通常是:

  1. 一个 effect 只做一类同步动作
  2. 依赖显式写全
  3. 能放在事件回调里的逻辑,就不要硬塞进 effect

7. 闭包旧值问题怎么理解

这是 React 里非常常见、也特别容易把人绕进去的问题。

原因可以压成这 3 条:

  1. 函数组件每次渲染都会创建新的作用域
  2. 回调和 effect 捕获的是当次渲染时的变量快照
  3. 后续状态变了,但旧回调没更新,就会读到旧值

例如:

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

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(prev => prev + 1)}>{count}</button>;
}

这里 setInterval 里拿到的 count,通常一直是第一次渲染时的值。

常见解决方式:

  1. 补全依赖
  2. 用函数式更新
  3. useRef 保存最新值

8. useRef 到底在解决什么问题

useRef 最常见的两类用途是:

  1. 拿 DOM 引用
  2. 保存不会触发重新渲染的可变值

8.1 DOM 引用

jsx
function SearchInput() {
  const inputRef = useRef(null);

  return (
    <>
      <input ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>聚焦</button>
    </>
  );
}

8.2 保存最新值

jsx
function AutoSave({ value }) {
  const latestValueRef = useRef(value);

  useEffect(() => {
    latestValueRef.current = value;
  }, [value]);

  useEffect(() => {
    const id = setInterval(() => {
      console.log("save", latestValueRef.current);
    }, 5000);

    return () => clearInterval(id);
  }, []);
}

这里 useRef 的价值是,让某个值跨渲染保留下来,但又不因为变化而触发重新渲染。


9. useContext 到底在解决什么问题

useContext 用来读取 Context 里的共享上下文。

它最适合解决的是,跨很多层组件传相同数据时,不想层层透传 props。

例如:

jsx
const ThemeContext = createContext("light");

function Page() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  const theme = useContext(ThemeContext);

  return <div>theme: {theme}</div>;
}

它适合:

  1. 主题
  2. 当前登录用户
  3. 语言信息
  4. 配置上下文

但它不等于完整状态管理框架。


10. 基础 Hooks 最容易混的边界

Hook更适合做什么
useState组件本地状态
useEffect副作用同步
useRefDOM 引用、可变值容器
useContext读取共享上下文

如果把它们用一句话串起来:

  1. useState 管内部状态
  2. useEffect 管副作用
  3. useRef 管不参与渲染的引用和值
  4. useContext 管跨层共享上下文

11. 一句话总结

React 基础 Hooks 的主线,本质上是在解决 函数组件如何拥有状态、如何同步副作用、如何跨渲染保存引用,以及如何读取共享上下文。

基于 VitePress 构建的个人技术笔记。