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。