🐸 青蛙编程网
首页 / 前端 / React / 5. Hooks:useState 与 useEffect

5. Hooks:useState 与 useEffect

这是什么:Hook = 在函数组件里「借」React 能力的函数,useState 借记忆,useEffect 借副作用。

为什么用:上一章只能在渲染过程中算数据,但改页面标题、开定时器、请求数据这些「组件之外的事」必须另外找地方做,Hook 正是干这个的。

如何用:本章写一个「可停的自动计数器」感受副作用与清理,再用空依赖数组做一次挂载时的网络请求。

5.1 useEffect:可停的自动计数器

副作用与清理函数一页打通(改页面标题 + 定时器 + 清理),整段可跑:

<!DOCTYPE html>
<html lang='zh-CN'>
<head>
  <meta charset='utf-8'>
  <script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
  <script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
  <script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
  <div id='root'></div>

  <script type='text/babel' data-presets='react'>
    function Timer() {
      const [count, setCount] = React.useState(0)
      const [running, setRunning] = React.useState(false)

      React.useEffect(() => {
        document.title = `已点 ${count} 次`     // 副作用:改外部世界(页面标题)
      }, [count])                               // 依赖数组:count 变了才执行

      React.useEffect(() => {
        if (!running) return                    // 停止状态:不挂定时器
        const timer = setInterval(() => {
          setCount((c) => c + 1)                // 新值依赖旧值 → 永远传函数
        }, 500)
        return () => clearInterval(timer)       // 清理:停止或卸载时打扫
      }, [running])

      return (
        <div>
          <h2>计数:{count}</h2>
          <button onClick={() => setRunning(!running)}>
            {running ? '停止' : '开始'}
          </button>
          <p>浏览器标签上的标题也在跟着变——那个就是「页面外」的副作用</p>
        </div>
      )
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<Timer />)
  </script>
</body>
</html>

你会看到:点「开始」每半秒自动加一,浏览器标签标题同时跟着变;点「停止」立刻打住——干净利落,全靠那个 return 的清理函数。

读法:「当 [ ] 里的依赖变化后,执行箭头函数」。

5.2 依赖数组的三种形态(必背)

写法 执行时机 用途
useEffect(fn) 每次渲染后都执行 几乎不这么用
useEffect(fn, []) 只在挂载后一次 初始拉数据、订阅
useEffect(fn, [a, b]) a 或 b 变了就执行 联动副作用

看到 [] 想到「onMounted」(Vue 的 onMounted 同学应心领神会)。

5.3 挂载时请求一次:真实网络请求

空数组 + fetch 的实战场,整段可跑(数据来自 npmmirror 的公开 registry 接口,与本章 CDN 同源体系):

<!DOCTYPE html>
<html lang='zh-CN'>
<head>
  <meta charset='utf-8'>
  <script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
  <script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
  <script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
  <div id='root'></div>

  <script type='text/babel' data-presets='react'>
    function PackageCard() {
      const [pkg, setPkg] = React.useState(null)

      React.useEffect(() => {
        fetch('https://registry.npmmirror.com/@babel/standalone/7.29.11')
          .then((res) => res.json())
          .then((data) => setPkg(data))   // 异步回来再 set,页面自动更新
      }, [])                              // 空数组:挂载时请求一次

      return (
        <div>
          <p>npmmirror 实时数据:</p>
          {pkg && (
            <ul>
              <li key='1'>包名:{pkg.name}</li>
              <li key='2'>版本:{pkg.version}</li>
              <li key='3'>简介:{pkg.description}</li>
            </ul>
          )}
        </div>
      )
    }

    ReactDOM.createRoot(document.getElementById('root')).render(<PackageCard />)
  </script>
</body>
</html>

你会看到:页面先只有「npmmirror 实时数据:」一行,一两秒后长出包名、版本号和简介——请求是异步的,数据回来 set 一下,视图自动跟上。

5.4 常见坑

  1. 死循环:deps 里漏依赖 → 状态引用自己 → 反复渲染。解决:写全依赖
  2. 对象/数组依赖:引用每渲染一次都变 → 视为「变了」无限循环。解决:拆到基本值
  3. 不该把「计算」放 useEffect:能由数据直接算出(Vue 里的 computed 一样)的交给渲染期直接计算或 useMemo

5.5 动手改

  1. 给 5.1 的 Timer 把间隔改成 1000;再把「开始/停止」逻辑再读一遍,说出清理函数在哪一刻执行
  2. 把每次 count 更新写入 localStorage(useEffect + deps [count])
  3. 把 5.3 接口地址里的 7.29.11 换成 18.3.1 所在的另一个包路径(如 react/18.3.1),看卡片数据怎么变

5.6 小结

  • useEffect 管副作用,deps 决定时机:每次 / 一次 / 按依赖
  • 会泄漏的副作用记得 return 清理函数(5.1 的 setInterval 就是最好的例子)
  • 更新值依赖旧值时用「函数式 setState」setX((v) => ...);派生数据别放 effect

下一章:列表渲染与条件渲染 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。