Skip to content

useEffect

介绍

useEffect 是 React 中的一个 Hook,用于在函数组件中处理副作用。副作用包括数据获取、订阅、手动修改 DOM 以及其他需要在组件渲染后执行的操作。

基本使用

语法

js
useEffect(setup, dependencies?)

参数:

  1. setupsetup 是一个函数,该函数定义了要执行的副作用代码
  2. dependencies(可选):dependencies 是依赖数组,用于控制副作用的执行时机。

用法

无依赖数组

如果没有第二个参数,useEffect 会在每次渲染后执行:

js
useEffect(() => {
  console.log('每次渲染后都会执行')
})

提示

可以类比于 vue 中的 onUpdated 生命周期钩子。

空依赖数组 []

如果传入空数组,useEffect 只会在组件首次渲染时执行一次。

jsx
import { useState, useEffect } from 'react'

function App() {
  const [pageTitle, setPageTitle] = useState('')

  useEffect(() => {
    setPageTitle(document.title)
  }, [])

  return <p>Page title: {pageTitle}</p>
}

export default App

提示

可以类比于 vue 中的 onMounted 生命周期钩子。

有依赖数组 [dep1, dep2, ...]

可以在数组中指定多个依赖项。只有当数组中的依赖发生变化时,useEffect 才会重新执行。

jsx
import { useState, useEffect } from 'react'

function App() {
  const [count] = useState(10)
  const [doubleCount, setDoubleCount] = useState(count * 2)

  useEffect(() => {
    setDoubleCount(count * 2)
  }, [count])

  return <div>{doubleCount}</div>
}

export default App

提示

可以类比于 vue 中的 watch 侦听器。

清理副作用

在副作用中,如果涉及到订阅、计时器等需要清理的操作,可以返回一个清理函数。这个清理函数会在组件卸载或下次执行副作用之前运行:

jsx
import { useState, useEffect } from 'react'

function App() {
  const [time, setTime] = useState(new Date().toLocaleTimeString())

  useEffect(() => {
    const timer = setInterval(() => {
      setTime(new Date().toLocaleTimeString())
    }, 1000)

    return () => clearInterval(timer)
  }, [])

  return <p>Current time: {time}</p>
}

export default App

提示

清理函数 可以类比于 vue 中的 onUnmounted 生命周期钩子。

useEffect 与 useLayoutEffect 的执行时机?

React 完整的渲染周期:

  1. 状态变化触发
  2. 渲染阶段
  3. 计算虚拟 DOM
  4. 提交阶段
  5. 更新真实 DOM
  6. useLayoutEffect 执行
  7. 浏览器绘制
  8. useEffect 执行

useEffect 执行时机

useEffect 不阻塞浏览器渲染的副作用渲染

  1. 触发渲染:组件状态(state)或者 props 变化
  2. 渲染阶段:计算新的虚拟 DOM
  3. 提交阶段:更新真实 DOM
  4. 浏览器绘制:用户看到更新后的界面
  5. useEffect 执行:异步处理

useLayoutEffect 执行时机

useLayoutEffect 在浏览器绘制前的精准控制

  1. 触发渲染:组件状态(state)或者 props 变化
  2. 渲染阶段:计算新的虚拟 DOM
  3. 提交阶段:更新真实 DOM
  4. useLayoutEffect 执行:同步处理
  5. 浏览器绘制:用户看到更新后的界面

核心区别

特性useEffectuseLayoutEffect
执行时机浏览器绘制后浏览器绘制前
执行方式异步同步
对渲染的影响不阻塞渲染阻塞渲染
适用场景数据获取、事件监听、订阅等副作用DOM 测量、布局调整、避免闪烁

useEffect 依赖项有什么用?

常见的副作用场景:

  1. 数据获取:从服务端获取数据
  2. DOM 操作:手动修改 DOM
  3. 事件监听:添加和移除事件监听器
  4. 定时器:设置和清除定时器
  5. ...

这就引出一个关键问题,这些副作用什么时候执行?

依赖项数组的作用就是告诉 React 何时重新执行副作用:

  1. 无数组:每次渲染后都执行
  2. 空数组 []:只在组件挂载和卸载时执行一次
  3. 有数组 [dep1, dep2]:只有当 dep1dep2 发生变化时才会执行

其比较原理就是使用 Object.is 来进行逐项比较。

❌ 常见陷阱:对象依赖项

options 每次渲染都是新对象,导致 Effect 重复执行:

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

  // 每次渲染都会重新创建对象
  const options = { step: 1 }

  useEffect(() => {
    console.log('Effect executed')
  }, [options])
}

解决方案:

jsx
const options = { step: 1 }

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

  useEffect(() => {
    console.log('Effect executed')
  }, [options]) // 现在 options 是稳定的
}
jsx
function Counter() {
  const [step, setStep] = useState(1)

  // 使用 useMemo 来稳定对象引用
  const options = useMemo(() => ({ step }), [step])

  useEffect(() => {
    console.log('Effect executed')
  }, [options]) // 现在 options 是稳定的
}

❌ 常见陷阱:函数依赖项

jsx
function SearchComponent({ query }) {
  const [results, setResults] = useState([])

  // 每次渲染都会重新创建函数
  const searchHandler = (searchQuery) => {
    return fetch(`/api/search?q=${searchQuery}`)
      .then((res) => res.json())
      .then((data) => setResults(data.items))
  }

  useEffect(() => {
    searchHandler(query)
  }, [query, searchHandler])
}

解决方案:

jsx
function SearchComponent({ query }) {
  const [results, setResults] = useState([])

  useEffect(() => {
    const searchHandler = (searchQuery) => {
      return fetch(`/api/search?q=${searchQuery}`)
        .then((res) => res.json())
        .then((data) => setResults(data.items))
    }

    searchHandler(query)
  }, [query]) // 现在不依赖于外部函数
}
jsx
function SearchComponent({ query }) {
  const [results, setResults] = useState([])

  // 使用 useCallback 来稳定函数引用
  const searchHandler = useCallback((searchQuery) => {
    return fetch(`/api/search?q=${searchQuery}`)
      .then((res) => res.json())
      .then((data) => setResults(data.items))
  }, []) // 函数不依赖于任何外部变量

  useEffect(() => {
    searchHandler(query)
  }, [query, searchHandler]) // 现在searchHandler 是稳定的
}

useEffect 中处理异步请求

❌ 错误示例:直接使用 async / await

jsx
useEffect(async () => {
  const result = await fetchData(id)
  setData(result)
}, [id])

为什么不行?

  1. 违反了 useEffect 规则:
    • useEffect 要不返回任何东西,要么返回清理函数。
    • async 函数隐式返回 Promise,违反设计规则。
  2. 没有解决“竞态条件”
    • 仍然可能出现请求顺序错乱的问题
    • 组件卸载后任然可能尝试更新状态

解决方案:

jsx
useEffect(() => {
  // 1. 设置标记
  let isMounted = true

  ;(async () => {
    const result = await fetchData(id)
    // 3. 更新状态前检查标记
    if (isMounted) {
      setData(result)
    }
  })()

  return () => {
    // 2. 清理时重置标记
    isMounted = false
  }
}, [id])
jsx
useEffect(() => {
  // 1. 创建 AbortController 实例
  const controller = new AbortController()

  ;(async () => {
    try {
      const result = await fetchData(
        id,
        // 2. 传递 signal 用于取消请求
        { signal: controller.signal },
      )
      setData(result)
    } catch (error) {
      if (error.name === 'AbortError') {
        // 请求被终止是预期行为
      } else {
        // 处理其他错误
      }
    }
  })()

  return () => {
    // 3. 清理函数中终止请求
    controller.abort()
  }
}, [id])

基于 MIT 许可发布