useEffect、useEffectEvent

本文由 AI 协助更新

本文介绍两个与副作用相关的 Hook:useEffect(执行副作用)和 useEffectEvent(在副作用中读取最新值且不参与触发)。


useEffect

是什么、何时执行

useEffect 用于在函数组件中执行副作用,例如网络请求、本地存储、订阅等。

副作用在本次渲染完成之后执行,不会阻塞本次渲染。

基本用法与依赖数组

我们给 useEffect 传两个参数:一个副作用函数,一个依赖数组。依赖数组表示这次副作用要执行的原因:只有这里列出的值发生变化时,才会重新执行该副作用。

import React, { useEffect, useState } from 'react'
import { View, Text, FlatList, TextInput, Button, StyleSheet } from 'react-native'

function App() {
  const [todos, setTodos] = useState<string[]>([])
  const [inputText, setInputText] = useState('')

  function handleAddTodo() {
    if (inputText.trim()) {
      setTodos((prev) => [...prev, inputText])
      setInputText('')
    }
  }

  useEffect(() => {
    console.log(`当前有 ${todos.length} 个待办事项`)
    // 这里可以执行数据持久化到本地存储
  }, [todos])

  return (
    <View style={styles.container}>
      <Text style={styles.title}>待办事项: {todos.length}</Text>
      <FlatList
        data={todos}
        renderItem={({ item }) => <Text style={styles.todoItem}>{item}</Text>}
        keyExtractor={(item, index) => index.toString()}
      />
      <TextInput
        value={inputText}
        onChangeText={setInputText}
        placeholder="输入新的待办事项"
        style={styles.input}
      />
      <Button title="添加" onPress={handleAddTodo} />
    </View>
  )
}











 
 
 
 
 





















上面第 15 行:依赖数组是 [todos],表示「只有 todos 变化时才执行这段副作用」。因此当仅 inputText 等其它状态变化时,这段副作用不会重新执行。

多个状态时:依赖列表只写「原因」

若组件里还有别的状态(例如 filter),只要不放进依赖数组,它们的变化就不会触发该副作用:

function App() {
  const [todos, setTodos] = useState<string[]>([])
  const [inputText, setInputText] = useState('')
  const [filter, setFilter] = useState('all')

  function handleAddTodo() { /* ... */ }

  useEffect(() => {
    console.log(`当前有 ${todos.length} 个待办事项`)
    // 数据持久化
  }, [todos])  // 只写「原因」:todos 变化才执行;filter / inputText 变化不执行

  // ...
}





 








这样,依赖列表就只表达「谁变化了才跑这段副作用」,语义更清晰。

用 useEffectEvent 分离「原因」和「行为」

有时你希望:只在部分值(如 todos)变化时执行副作用,但在副作用里又要用到另一些值(如 filter)的最新值。若把 filter 也写进依赖,副作用会在 filter 变化时多执行一次;若不写,又会遇到闭包里拿到旧值的问题。

这时可以用下一节的 useEffectEvent:把「要在副作用里读的最新值」包在 useEffectEvent 里,依赖数组里只保留「真正触发执行的原因」。例如:

import { useEffect, useEffectEvent } from 'react'

function App() {
  const [todos, setTodos] = useState<string[]>([])
  const [filter, setFilter] = useState('all')

  const onSaveTodos = useEffectEvent(() => {
    console.log(`保存 ${todos.length} 个待办事项,当前筛选: ${filter}`)
    // 保存到本地存储,这里总能拿到最新的 todos 和 filter
  })

  useEffect(() => {
    onSaveTodos()
  }, [todos])  // 原因:只有 todos 变化才触发保存;onSaveTodos 内部可读最新 filter
}

依赖数组只写 [todos],语义是「只有 todos 变化才保存」;而保存逻辑里又能访问到最新的 filter,无需把 filter 放进依赖。useEffectEvent 的用法和注意点见下一节。

清理函数(cleanup)

若副作用需要清理(如定时器、订阅),在副作用函数里返回一个函数即可。该函数会在「组件卸载」或「下一次本副作用执行前」被调用。

useEffect(() => {
  const timer = setInterval(() => {
    console.log(`当前有 ${todos.length} 个待办事项`)
  }, 5000)

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

尽早返回

若副作用需在满足条件时才执行,建议在函数开头做判断并尽早返回,逻辑更清晰:

useEffect(() => {
  if (todos.length === 0) {
    return
  }
  console.log(`有效的待办列表,共 ${todos.length}`)
}, [todos])

useEffect 小结

  • 依赖数组表示副作用执行的原因,应只列出「变化了就要重新跑」的值。
  • 始终显式写出依赖,哪怕为 []
  • 一个 useEffect 尽量只做一件事。
  • 若需要在副作用里读某值又不希望该值成为触发原因,用下一节的 useEffectEvent 分离原因与行为。

useEffectEvent

是什么、解决什么问题

useEffectEvent 是 React 19 提供的 Hook,用来在不把某值写进 useEffect 依赖的前提下,在副作用里始终读到该值的最新版本,从而既避免多余执行,又避免闭包旧值。

const onEvent = useEffectEvent(callback)
  • 返回一个函数,引用稳定(类似 useRef),不用放进 useEffect 的依赖数组。
  • 这个函数只能在 Effect 内部调用(不能放在渲染里、不能放在事件处理器里、不能从自定义 Hook 里导出)。
  • 在 Effect 里调用时,callback 内部总能拿到当前最新的 props / state

何时用:当你在 useEffect 里需要读取某些值,又不想让这些值的变化触发该 Effect 重新执行时,用 useEffectEvent 包一层,在 Effect 里只调用这个「Event 函数」。

典型场景

1. 日志 / 分析:只随部分数据触发,但要带上最新上下文

function App() {
  const [todos, setTodos] = useState<string[]>([])
  const [userName, setUserName] = useState('用户')

  const onLog = useEffectEvent(() => {
    console.log(`${userName} 当前有 ${todos.length} 个待办事项`)
  })

  useEffect(() => {
    onLog()
  }, [todos])  // 只在 todos 变化时打日志,userName 变化不触发,但日志里是最新 userName
}

2. 定时器里用最新状态

function App() {
  const [todos, setTodos] = useState<string[]>([])

  const onCheckTodos = useEffectEvent(() => {
    console.log(`检查待办: ${todos.length}`)
  })

  useEffect(() => {
    const interval = setInterval(() => {
      onCheckTodos()
    }, 10000)
    return () => clearInterval(interval)
  }, [])  // 空依赖,定时器不重建,但每次执行都能读到最新 todos
}

3. 自动保存 + 手动保存共用一套逻辑

需求:todos 变化时自动保存,同时提供「保存」按钮;保存逻辑一致,且保存时能拿到最新状态。

import { useEffectEvent } from 'react'

function App() {
  const [todos, setTodos] = useState<string[]>([])
  const [saveCount, setSaveCount] = useState(0)

  // 核心业务:保存(可在任意地方调用)
  const saveTodos = () => {
    console.log(`保存 ${todos.length} 个待办事项`)
    setSaveCount((c) => c + 1)
  }

  // 在 Effect 里调用时,用 useEffectEvent 包一层,依赖数组只写「原因」
  const onAutoSave = useEffectEvent(() => {
    saveTodos()
  })

  useEffect(() => {
    onAutoSave()
  }, [todos])

  const handleSave = () => {
    saveTodos()
  }

  return (
    <View style={styles.container}>
      <Text>保存次数: {saveCount}</Text>
      <Button title="保存" onPress={handleSave} />
      {/* ... */}
    </View>
  )
}
  • 核心逻辑saveTodos):纯业务,事件和 Effect 都可调。
  • Effect 里调用onAutoSave):用 useEffectEvent 包装,依赖只写 [todos]
  • 事件处理handleSave):直接调 saveTodos

命名建议:核心函数用动词(saveTodos);Effect 里用的用 on 前缀(onAutoSave);事件处理器用 handle 前缀(handleSave)。

特点与限制

特点:

  • 返回的函数引用稳定,不必放进依赖数组。
  • 在 Effect 内调用时,内部总能访问最新 props / state。
  • 有助于把「副作用的触发原因」和「副作用里要用的最新值」分开表达。

限制:

  • 只能在 useEffect(或其它 Effect)内部调用,不能在渲染阶段、不能在事件处理器里调用。
  • 不能从自定义 Hook 中把 useEffectEvent 返回的函数暴露出去。
// ✅ 在 useEffect 中调用
useEffect(() => {
  onAutoSave()
}, [todos])

// ❌ 渲染期间
return <Text>{onAutoSave()}</Text>

// ❌ 事件处理器
<Button onPress={onAutoSave} />

// ❌ 从自定义 Hook 导出
function useTodos() {
  const onAutoSave = useEffectEvent(() => {...})
  return { onAutoSave }  // 不允许
}

在事件处理器里若要在异步回调中拿到最新值,应使用 ref 方案,不要用 useEffectEvent


目录

  1. 组件 — 认识组件和元素、Props、State、单向数据流
  2. useState、useReducer、useContext
  3. useEffect、useEffectEvent
  4. 闭包陷阱、useRef
  5. useCallback、useMemo、React.memo
  6. Hook 规则、自定义 Hook
上次更新: