将事件从 Effect 中分开
🎯 事件处理函数vs Effect的本质区别
💡 事件处理函数特性
- 🖱️ 手动触发:只在用户特定交互时运行(如点击按钮)
- 🚫 非响应式:不会因为其中使用的props或state变化而自动重新执行
- 🎮 处理交互:用于响应特定的用户操作
jsx
function handleClick() {
// 只在用户点击时运行
sendMessage(message);
}
🔄 Effect特性
- 🤖 自动触发:在渲染后运行并在依赖变化时重新运行
- ✅ 响应式:当其依赖的props或state变化时自动重新执行
- 🔄 保持同步:用于将组件与外部系统同步
jsx
useEffect(() => {
// 自动连接到聊天室,并在roomId变化时重新连接
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
🧪 响应式逻辑vs非响应式逻辑
📊 响应式值
在React中,以下值被视为"响应式":
- ⚛️ Props - 可能在重新渲染时改变
- ⚛️ State - 可能在重新渲染时改变
- ⚛️ 由它们派生的值 - 依赖props或state计算出的值
🔍 代码中的响应性差异
事件处理函数中的逻辑 - 非响应式:
jsx
function ChatRoom({ roomId, theme }) {
function handleSendMessage() {
// 🔴 非响应式:theme变化不会触发重新发送
sendMessage('Hello!', theme);
}
// ...
}
Effect中的逻辑 - 响应式:
jsx
function ChatRoom({ roomId, theme }) {
useEffect(() => {
// 🔴 响应式:theme变化会导致重新连接
const connection = createConnection(serverUrl, roomId, theme);
connection.connect();
return () => connection.disconnect();
}, [roomId, theme]); // 依赖theme
// ...
}
🪄 使用Effect Event分离响应式和非响应式逻辑
🛠️ useEffectEvent的用途
- 📦 在Effect中封装非响应式逻辑
- 🚫 使某些值不触发Effect重新执行
- 🔄 始终获取最新的props/state值而不使Effect重新执行
jsx
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// ✅ 非响应式逻辑,不会成为依赖
const onConnected = useEffectEvent(roomName => {
// 这里使用的theme始终是最新值
showNotification(`连接到${roomName}`, theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
// 调用Effect Event
onConnected(connection.roomName);
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // 不需要依赖theme和onConnected
// ...
}
📝 useEffectEvent的注意事项
- 🚨 仅实验性API:目前尚未在React稳定版中发布
- 🏠 仅在Effect内调用:不要在其他地方调用Effect Event
- 🚫 不能传递给其他组件:只在定义它的组件内使用
- ⚡ 解决依赖项过多问题:替代手动排除依赖项的方法
🔮 Effect Event的工作原理
Effect Event是对React中两种行为模式的融合:
- 📊 保留了事件处理函数的非响应式特性(可访问最新props/state)
- 🔄 可以在Effect的响应式环境中调用(与Effect生命周期绑定)
jsx
// Effect Event可以读取到最新的值
const onTick = useEffectEvent(() => {
// 读取到最新的count值
setCount(count + 1);
});
// 在响应式Effect中使用
useEffect(() => {
const id = setInterval(() => {
onTick(); // 调用Effect Event
}, 1000);
return () => clearInterval(id);
}, []); // 不需要把count作为依赖项
⚠️ 适用场景示例
案例1:计时器计数
jsx
function Timer() {
const [count, setCount] = useState(0);
const [increment, setIncrement] = useState(1);
// 使用Effect Event读取最新的increment值
const onTick = useEffectEvent(() => {
setCount(c => c + increment);
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // 使用最新的increment
}, 1000);
return () => clearInterval(id);
}, []); // 不需要increment作为依赖
// ...
}
案例2:记录页面访问
jsx
function Page({ url }) {
const onVisit = useEffectEvent(visitedUrl => {
logAnalytics('visit', { url: visitedUrl, sessionId });
});
useEffect(() => {
onVisit(url);
}, [url]); // sessionId不需要作为依赖项
// ...
}
📝 总结
- 🎭 事件处理函数在特定交互时执行,Effect在需要同步时执行
- 🧩 事件处理函数中的逻辑是非响应式的,Effect中的逻辑是响应式的
- 🔀 有时候需要在Effect中混合使用响应式和非响应式逻辑
- 📦
useEffectEvent
允许你在Effect内部定义非响应式代码片段 - 🔍 Effect Event可以读取到最新的props和state而不成为Effect的依赖
- 🚫 只在Effect内部调用Effect Event,不要传递给其他组件
- 🧪 Effect Event是将非响应式逻辑从Effect中分离出来的最佳实践