深入理解 React Hooks:useState 与 useEffect 的核心原理与实践

74 阅读5分钟

“状态驱动视图,副作用管理生命周期。”
—— React 开发者的日常信条 💡

今天我们将围绕 React Hooks 中最基础也最重要的两个函数 —— useStateuseEffect,深入剖析它们的使用方式、底层思想以及常见误区。

🔹 第一部分:纯函数 vs 副作用 —— 理解 React 的“纯净”哲学

在进入 Hooks 之前,我们必须先理解一个关键概念:纯函数(Pure Function)

const add = function (x, y) {
  return x + y;
}

这是一个典型的纯函数:

  • 相同输入 → 相同输出 ✅
  • 不修改外部变量 ❌
  • 不发起网络请求 ❌
  • 不操作 DOM ❌

而下面这段代码就不是纯函数

function add(nums) {
  nums.push(3); // 修改了外部数组!
  return nums.reduce((pre, cur) => pre + cur, 0);
}

它产生了副作用(Side Effect) —— 改变了传入的 nums 数组。这在 React 函数组件中是大忌,因为组件本身应尽可能是一个纯函数:接收 props,返回 JSX。

🎯 React 组件 = 纯函数(理想状态)
但现实世界充满副作用:API 请求、定时器、订阅事件……怎么办?

答案就是:useEffect 来隔离副作用

❓答疑解惑:为什么 React 强调“纯函数”?

Q:组件为什么不能有副作用?

A:因为 React 需要可预测性。如果每次渲染都偷偷改全局变量或发请求,调试将极其困难。通过把副作用集中到 useEffect,React 能精确控制它们的执行时机(挂载、更新、卸载),保证渲染过程干净、可复现。

Q:fetch 是异步的,能放进纯函数吗?

A:不能!所以 fetch 必须放在 useEffect 中。这也是为什么你在 useState 的初始化函数里不能写异步逻辑——它必须是同步纯函数。

🔹 第二部分:useState —— 让函数组件拥有“记忆”

React 函数组件默认是无状态的。但通过 useState,我们可以赋予它响应式状态

const [num, setNum] = useState(() => {
  const num1 = 1 + 2;
  const num2 = 2 + 3;
  return num1 + num2; // 同步计算初始值
});

这里的关键点:

  • useState 接收一个初始值,可以是直接值(如 1),也可以是一个同步函数(用于复杂计算)。
  • 返回一个数组:[当前状态, 更新函数]
  • setNum 可以接受新值,也可以接受一个函数:setNum(prev => prev + 1),这能避免闭包陷阱。

💡 状态的本质:变化的数据。用户点击 → 触发 setNum → 状态更新 → 组件重新渲染 → 视图变化。

❓答疑解惑:关于 useState 的常见疑问

Q:为什么 useState 的初始化函数必须是同步的?

A:因为 React 需要在首次渲染时立即确定初始状态。如果允许异步(比如 await fetch()),组件就无法知道该显示什么,会卡住。异步数据应在 useEffect 中获取,再用 setNum 更新。

Q:为什么推荐用 setNum(prev => prev + 1) 而不是 setNum(num + 1)

A:这是为了避免闭包问题。在事件处理器或异步回调中,num 可能是旧值(被闭包捕获)。而 prev 总是 React 内部最新的状态值,更安全。

Q:状态更新是同步还是异步?

A:在 React 事件处理器中,setNum批量异步更新的(为了性能优化)。但在原生事件或 setTimeout 中,可能是同步的。不过你不应依赖其同步性,应始终当作异步处理。

🔹 第三部分:useEffect —— 副作用的“管家”

当你的组件需要:

  • 发起 API 请求 🌐
  • 设置定时器 ⏰
  • 订阅事件 📡
  • 手动操作 DOM 🖱️

这些副作用必须交给 useEffect

useEffect(() => {
  console.log('effect');
  const timer = setInterval(() => {
    console.log(num);
  }, 1000);

  return () => {
    console.log('remove');
    clearInterval(timer); // 清理上一次的副作用
  };
}, [num]);

关键机制:

  • 第一个参数:副作用函数(可包含异步逻辑)
  • 第二个参数:依赖数组,决定何时重新执行
  • 返回函数:清理函数,在下一次 effect 执行前或组件卸载时调用

三种依赖情况:

  1. []:只在挂载时执行一次(类似 componentDidMount
  2. [a, b]:当 ab 变化时执行
  3. 不传:每次渲染后都执行(慎用!)

⚠️ 注意:useEffect 中的 num闭包捕获的值。如果依赖没写全,可能读到旧状态!

❓答疑解惑:useEffect 的高频困惑

Q:为什么定时器要清理?不清理会怎样?

A:如果不清理,组件卸载后定时器仍在运行,会尝试更新已卸载组件的状态,导致内存泄漏和警告。清理函数就是“善后”。

Q:依赖数组写错了会有什么后果?

A:两种典型错误:

  • 漏写依赖:effect 读取旧状态,逻辑出错。
  • 多写无关依赖:effect 频繁执行,性能下降。

建议开启 ESLint 的 react-hooks/exhaustive-deps 规则自动检查。

Q:能在 useEffect 里直接写 async/await 吗?

A:不能直接写,因为 useEffect 的第一个参数必须是普通函数(不能是 async 函数,因其返回 Promise)。正确做法:

useEffect(() => {
  const fetchData = async () => {
    const data = await queryData();
    setNum(data);
  };
  fetchData();
}, []);

🔚 总结:Hooks 的设计哲学

概念作用对应 Hook
状态存储变化的数据useState
副作用处理非纯操作(请求、定时器等)useEffect

React 通过这两个核心 Hook,将数据流副作用管理清晰分离:

  • 组件主体保持纯净(纯函数风格)
  • 副作用被隔离、受控、可清理

这正是现代 React 应用可维护、可测试、高性能的基石 🏗️。


🌟 最后提醒

  • 初始化状态 → 用 useState + 同步纯函数
  • 获取异步数据 → 用 useEffect + fetch/axios
  • 清理资源 → 别忘了 return () => {}
  • 依赖数组 → 写全、写对,让 ESLint 帮你

掌握这些,你就真正理解了 React Hooks 的“道”而非“术”🚀。

Happy coding! 💻✨