Appearance
useEffectEvent实验性Hook
学习目标
通过本章学习,你将掌握:
- useEffectEvent的设计动机和用途
- 响应式值和非响应式值的区分
- 解决Effect依赖问题的最佳方案
- useEffectEvent的使用场景
- 与useEffect、useCallback的对比
- 实战案例和最佳实践
- 常见问题和解决方案
- 迁移策略和注意事项
第一部分:问题背景
1.1 Effect依赖困境
在React中,useEffect的依赖数组经常导致困扰。
经典问题:
jsx
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
showNotification('Connected!', theme);
});
return () => connection.disconnect();
}, [roomId]); // 缺少theme依赖
// 问题:theme变化时不会重新连接
// 但连接成功的通知会显示旧的theme
}ESLint警告:
React Hook useEffect has a missing dependency: 'theme'.
Either include it or remove the dependency array.添加theme到依赖的问题:
jsx
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
showNotification('Connected!', theme);
});
return () => connection.disconnect();
}, [roomId, theme]); // 添加了theme
// 新问题:theme变化会导致重新连接
// 但我们只想在roomId变化时重新连接
// theme只用于显示通知,不应该触发重连
}问题分析:
困境:
1. 不添加theme到依赖
→ ESLint警告
→ 可能使用过期值(闭包陷阱)
→ 难以维护
2. 添加theme到依赖
→ 不必要的重新连接
→ 性能问题
→ 用户体验差(频繁断开重连)
需要:一种方式让Effect访问最新值,但不成为响应式依赖1.2 传统解决方案的局限
方案1:使用ref(不推荐)
jsx
function ChatRoom({ roomId, theme }) {
const themeRef = useRef(theme);
// 每次渲染更新ref
useEffect(() => {
themeRef.current = theme;
}, [theme]);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
// 从ref读取最新值
showNotification('Connected!', themeRef.current);
});
return () => connection.disconnect();
}, [roomId]); // 只依赖roomId
}
// 问题:
// 1. 需要额外的useEffect同步ref
// 2. 代码复杂,难以理解
// 3. 容易出错
// 4. 不符合React的声明式理念方案2:使用useCallback(不推荐)
jsx
function ChatRoom({ roomId, theme }) {
const showConnectedNotification = useCallback(() => {
showNotification('Connected!', theme);
}, [theme]); // theme变化会创建新函数
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', showConnectedNotification);
return () => connection.disconnect();
}, [roomId, showConnectedNotification]); // 必须依赖函数
// 问题:
// theme变化 → 新函数 → Effect重新运行 → 重新连接
// 还是没解决问题
}方案3:忽略ESLint警告(非常不推荐)
jsx
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
showNotification('Connected!', theme);
});
return () => connection.disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [roomId]); // 忽略theme的警告
// 问题:
// 1. 可能使用过期的theme值
// 2. 隐藏潜在bug
// 3. 难以维护
// 4. 团队协作问题
}1.3 useEffectEvent的解决方案
React团队设计的优雅解决方案:
jsx
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
// 创建Effect Event
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
onConnected(); // 调用Effect Event
});
return () => connection.disconnect();
}, [roomId]); // 只依赖roomId,不需要包含onConnected
// 效果:
// ✅ theme变化不会重新连接
// ✅ 总是使用最新的theme值
// ✅ 无ESLint警告
// ✅ 代码清晰易懂
}工作原理:
useEffectEvent创建的函数:
1. 总是访问最新的props/state
2. 不是响应式的(不触发Effect重新运行)
3. 只能在Effect内部调用
4. 类似于事件处理器,但在Effect中使用
对比:
- useCallback:响应式,依赖变化会创建新函数
- useEffectEvent:非响应式,总是最新但不触发更新第二部分:基本用法
2.1 基础语法
useEffectEvent的基本使用模式:
jsx
import { useEffect, useEffectEvent } from 'react';
function Component({ reactiveValue, nonReactiveValue }) {
// 1. 使用useEffectEvent包装非响应式逻辑
const handleEvent = useEffectEvent(() => {
// 可以访问最新的nonReactiveValue
console.log('Latest value:', nonReactiveValue);
});
// 2. 在useEffect中调用
useEffect(() => {
// 响应式逻辑
const subscription = subscribe(reactiveValue);
subscription.on('event', () => {
handleEvent(); // 调用Effect Event
});
return () => subscription.unsubscribe();
}, [reactiveValue]); // 只依赖响应式值
}完整示例:
jsx
function PageView({ url, referrer }) {
// referrer用于分析,但不应该触发重新记录
const logVisit = useEffectEvent(() => {
analytics.logVisit(url, referrer);
});
useEffect(() => {
// url变化时记录访问
logVisit();
}, [url]); // 只依赖url
return <Page url={url} />;
}
// 效果:
// - url变化:记录新的访问(使用最新的referrer)
// - referrer变化:不记录访问(因为url没变)2.2 访问最新state
useEffectEvent总是访问最新的state值:
jsx
function Counter() {
const [count, setCount] = useState(0);
const [intervalDelay, setIntervalDelay] = useState(1000);
// 增量逻辑访问最新count
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // 总是使用最新的count
}, intervalDelay);
return () => clearInterval(id);
}, [intervalDelay]); // 只在delay变化时重置定时器
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(0)}>Reset</button>
<select value={intervalDelay} onChange={e => setIntervalDelay(Number(e.target.value))}>
<option value={100}>Fast (100ms)</option>
<option value={1000}>Normal (1s)</option>
<option value={5000}>Slow (5s)</option>
</select>
</div>
);
}
// 对比useCallback的问题:
function CounterWithCallback() {
const [count, setCount] = useState(0);
const [intervalDelay, setIntervalDelay] = useState(1000);
const onTick = useCallback(() => {
setCount(count + 1); // 闭包陷阱!count永远是初始值
}, []); // 空依赖导致闭包问题
// 或者
const onTick = useCallback(() => {
setCount(count + 1);
}, [count]); // count变化会创建新函数,导致重置定时器
useEffect(() => {
const id = setInterval(onTick, intervalDelay);
return () => clearInterval(id);
}, [intervalDelay, onTick]); // 必须依赖onTick
}2.3 访问最新props
useEffectEvent访问最新props:
jsx
function ChatRoom({ roomId, onReceiveMessage }) {
// onReceiveMessage可能每次渲染都不同
const onMessage = useEffectEvent((receivedMessage) => {
onReceiveMessage(receivedMessage);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg); // 调用最新的onReceiveMessage
});
return () => connection.disconnect();
}, [roomId]); // 不需要包含onReceiveMessage
return <div>Chat Room: {roomId}</div>;
}
// 使用示例
function App() {
const [notifications, setNotifications] = useState([]);
return (
<ChatRoom
roomId="general"
onReceiveMessage={(msg) => {
// 这个函数每次渲染都不同
setNotifications(prev => [...prev, msg]);
showToast(msg);
}}
/>
);
}第三部分:典型使用场景
3.1 数据分析和日志
记录用户行为,但不想频繁触发Effect:
jsx
function ProductPage({ productId, user, analytics }) {
// analytics配置可能频繁变化,但不应触发重新记录
const logView = useEffectEvent(() => {
analytics.logProductView({
productId,
userId: user.id,
timestamp: Date.now(),
referrer: document.referrer
});
});
useEffect(() => {
// 只在productId变化时记录
logView();
}, [productId]);
return <ProductDetails productId={productId} />;
}复杂分析场景:
jsx
function AnalyticsTracker({
eventName,
eventData,
userId,
sessionId,
metadata
}) {
// metadata可能包含频繁变化的数据(如timestamp)
const trackEvent = useEffectEvent(() => {
analytics.track(eventName, {
...eventData,
userId,
sessionId,
metadata,
clientTime: Date.now(),
userAgent: navigator.userAgent
});
});
useEffect(() => {
// 只在事件名称或核心数据变化时记录
trackEvent();
}, [eventName, eventData]);
return null;
}
// 使用
function App() {
const [pageViews, setPageViews] = useState(0);
const [currentPage, setCurrentPage] = useState('/home');
return (
<>
<AnalyticsTracker
eventName="page_view"
eventData={{ page: currentPage }}
userId="user123"
sessionId="session456"
metadata={{
pageViews, // 频繁变化,但不应触发重新记录
timestamp: Date.now()
}}
/>
<Navigation onNavigate={setCurrentPage} />
</>
);
}3.2 动画和视觉效果
控制动画而不重新触发:
jsx
function FadeInComponent({ content, duration, playSound }) {
const ref = useRef(null);
// duration用于动画,但不应该在duration变化时重新触发动画
const onAppear = useEffectEvent((animation) => {
animation.start(duration);
if (playSound) {
audio.play();
}
});
useEffect(() => {
const animation = new FadeInAnimation(ref.current);
onAppear(animation);
return () => animation.stop();
}, []); // 只在mount时触发动画
return (
<div ref={ref}>
{content}
</div>
);
}
// 使用
function App() {
const [duration, setDuration] = useState(1000);
const [show, setShow] = useState(false);
return (
<div>
<label>
Animation duration: {duration}ms
<input
type="range"
min="100"
max="3000"
value={duration}
onChange={e => setDuration(Number(e.target.value))}
/>
</label>
<button onClick={() => setShow(!show)}>
Toggle
</button>
{show && (
<FadeInComponent
content="Hello World"
duration={duration}
playSound={true}
/>
)}
</div>
);
}滚动动画:
jsx
function InfiniteScroll({ items, threshold, onLoadMore, pageSize }) {
const containerRef = useRef(null);
// pageSize变化不应重新绑定滚动监听
const loadNext = useEffectEvent(() => {
onLoadMore(pageSize);
});
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = container;
if (scrollHeight - scrollTop - clientHeight < threshold) {
loadNext();
}
};
container.addEventListener('scroll', handleScroll);
return () => container.removeEventListener('scroll', handleScroll);
}, [threshold]); // 只在threshold变化时重新绑定
return (
<div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
{items.map(item => <Item key={item.id} {...item} />)}
</div>
);
}3.3 外部系统集成
集成WebSocket、定时器等外部系统:
jsx
function RealtimeData({ endpoint, onDataReceived, reconnectInterval }) {
// onDataReceived可能频繁变化(如包含state更新函数)
const handleData = useEffectEvent((data) => {
onDataReceived({
...data,
receivedAt: Date.now()
});
});
useEffect(() => {
let ws = null;
let reconnectTimer = null;
const connect = () => {
ws = new WebSocket(endpoint);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleData(data); // 使用最新的处理函数
};
ws.onerror = () => {
console.error('WebSocket error');
};
ws.onclose = () => {
// 自动重连
reconnectTimer = setTimeout(connect, reconnectInterval);
};
};
connect();
return () => {
if (ws) ws.close();
if (reconnectTimer) clearTimeout(reconnectTimer);
};
}, [endpoint, reconnectInterval]); // 不包含onDataReceived
return null;
}
// 使用
function Dashboard() {
const [data, setData] = useState([]);
const [notifications, setNotifications] = useState([]);
return (
<RealtimeData
endpoint="wss://api.example.com/data"
reconnectInterval={3000}
onDataReceived={(newData) => {
// 这个函数每次渲染都不同(因为访问了state)
setData(prev => [...prev, newData]);
setNotifications(prev => [...prev, `Received: ${newData.type}`]);
}}
/>
);
}定时器场景:
jsx
function AutoSave({ data, saveInterval, onSave }) {
// onSave可能包含最新的认证token等信息
const performSave = useEffectEvent(async () => {
try {
await onSave(data);
console.log('Auto-saved successfully');
} catch (error) {
console.error('Auto-save failed:', error);
}
});
useEffect(() => {
const id = setInterval(() => {
performSave();
}, saveInterval);
return () => clearInterval(id);
}, [saveInterval]); // 只在interval变化时重置定时器
return null;
}3.4 表单和用户输入
处理表单状态,避免不必要的Effect触发:
jsx
function SearchWithDebounce({ query, onSearch, debounceDelay, filters }) {
// filters可能频繁变化,但不应影响debounce
const performSearch = useEffectEvent(() => {
onSearch(query, filters);
});
useEffect(() => {
const timer = setTimeout(() => {
performSearch();
}, debounceDelay);
return () => clearTimeout(timer);
}, [query, debounceDelay]); // 不包含filters
return null;
}
// 使用
function SearchPage() {
const [query, setQuery] = useState('');
const [filters, setFilters] = useState({
category: 'all',
sortBy: 'relevance',
priceRange: [0, 1000]
});
const [results, setResults] = useState([]);
const handleSearch = async (searchQuery, searchFilters) => {
const data = await api.search(searchQuery, searchFilters);
setResults(data);
};
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<FilterPanel filters={filters} onChange={setFilters} />
<SearchWithDebounce
query={query}
onSearch={handleSearch}
debounceDelay={300}
filters={filters}
/>
<ResultsList results={results} />
</div>
);
}第四部分:高级模式
4.1 结合自定义Hook
在自定义Hook中使用useEffectEvent:
jsx
function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
// onReceiveMessage是非响应式的
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // 不包含onReceiveMessage
}
// 使用自定义Hook
function ChatApp() {
const [messages, setMessages] = useState([]);
const [unreadCount, setUnreadCount] = useState(0);
useChatRoom({
serverUrl: 'wss://chat.example.com',
roomId: 'general',
onReceiveMessage: (msg) => {
// 这个函数每次渲染都不同
setMessages(prev => [...prev, msg]);
setUnreadCount(prev => prev + 1);
document.title = `(${unreadCount + 1}) New Messages`;
}
});
return (
<div>
<MessageList messages={messages} />
</div>
);
}复杂自定义Hook:
jsx
function useDataSync({
source,
onUpdate,
onError,
pollInterval,
retryAttempts
}) {
// 回调函数都是非响应式的
const handleUpdate = useEffectEvent(onUpdate);
const handleError = useEffectEvent(onError);
useEffect(() => {
let cancelled = false;
let retries = 0;
const fetchData = async () => {
try {
const data = await fetch(source).then(r => r.json());
if (!cancelled) {
handleUpdate(data);
retries = 0; // 重置重试次数
}
} catch (error) {
if (!cancelled && retries < retryAttempts) {
retries++;
handleError(error, retries);
// 指数退避重试
setTimeout(fetchData, Math.pow(2, retries) * 1000);
}
}
};
fetchData();
const interval = setInterval(fetchData, pollInterval);
return () => {
cancelled = true;
clearInterval(interval);
};
}, [source, pollInterval, retryAttempts]);
}
// 使用
function Dashboard() {
const [data, setData] = useState(null);
const [errorLog, setErrorLog] = useState([]);
useDataSync({
source: '/api/dashboard',
pollInterval: 5000,
retryAttempts: 3,
onUpdate: (newData) => {
setData(newData);
console.log('Data updated:', newData);
},
onError: (error, attempt) => {
setErrorLog(prev => [...prev, { error, attempt, time: Date.now() }]);
console.error(`Fetch failed (attempt ${attempt}):`, error);
}
});
return (
<div>
{data ? <DataDisplay data={data} /> : <Loading />}
{errorLog.length > 0 && <ErrorLog errors={errorLog} />}
</div>
);
}4.2 条件逻辑
useEffectEvent中的条件逻辑:
jsx
function NotificationManager({
message,
shouldNotify,
soundEnabled,
vibrationEnabled
}) {
// 通知配置是非响应式的
const notify = useEffectEvent(() => {
if (shouldNotify) {
showNotification(message);
if (soundEnabled) {
playNotificationSound();
}
if (vibrationEnabled && 'vibrate' in navigator) {
navigator.vibrate([200, 100, 200]);
}
}
});
useEffect(() => {
// 只在message变化时通知
notify();
}, [message]);
return null;
}复杂条件:
jsx
function ConditionalSync({
userId,
syncEnabled,
syncInterval,
filters,
onSyncSuccess,
onSyncFailure
}) {
const performSync = useEffectEvent(async () => {
if (!syncEnabled) {
console.log('Sync disabled, skipping');
return;
}
try {
const data = await syncUserData(userId, filters);
onSyncSuccess(data);
} catch (error) {
onSyncFailure(error);
}
});
useEffect(() => {
if (!syncEnabled) {
return; // 完全跳过Effect
}
performSync(); // 立即同步一次
const interval = setInterval(() => {
performSync();
}, syncInterval);
return () => clearInterval(interval);
}, [userId, syncEnabled, syncInterval]); // 不包含filters和回调
return null;
}4.3 错误处理
Effect中的错误处理:
jsx
function DataFetcher({ url, onSuccess, onError, retryConfig }) {
const handleSuccess = useEffectEvent(onSuccess);
const handleError = useEffectEvent(onError);
useEffect(() => {
let cancelled = false;
let retryCount = 0;
const fetchWithRetry = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!cancelled) {
handleSuccess(data);
}
} catch (error) {
if (cancelled) return;
if (retryCount < retryConfig.maxRetries) {
retryCount++;
const delay = retryConfig.baseDelay * Math.pow(2, retryCount - 1);
console.log(`Retrying in ${delay}ms (attempt ${retryCount})`);
setTimeout(fetchWithRetry, delay);
} else {
handleError({
message: error.message,
attempts: retryCount,
url
});
}
}
};
fetchWithRetry();
return () => {
cancelled = true;
};
}, [url]); // retryConfig是非响应式的
return null;
}
// 使用
function App() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
return (
<DataFetcher
url="/api/data"
retryConfig={{
maxRetries: 3,
baseDelay: 1000
}}
onSuccess={(data) => {
setData(data);
setError(null);
}}
onError={(errorInfo) => {
setError(errorInfo);
console.error('Failed after retries:', errorInfo);
}}
/>
);
}第五部分:对比其他方案
5.1 useEffectEvent vs useCallback
两者的本质区别:
jsx
// useCallback:响应式函数
function WithUseCallback({ count, onUpdate }) {
const handleClick = useCallback(() => {
onUpdate(count);
}, [count, onUpdate]); // 依赖变化会创建新函数
useEffect(() => {
button.addEventListener('click', handleClick);
return () => button.removeEventListener('click', handleClick);
}, [handleClick]); // 必须包含handleClick
// 问题:count或onUpdate变化 → 新函数 → Effect重新运行
}
// useEffectEvent:非响应式函数
function WithUseEffectEvent({ count, onUpdate }) {
const handleClick = useEffectEvent(() => {
onUpdate(count); // 总是最新值
});
useEffect(() => {
button.addEventListener('click', handleClick);
return () => button.removeEventListener('click', handleClick);
}, []); // 不需要依赖
// 效果:count或onUpdate变化 → 不重新绑定事件
}使用场景对比:
jsx
// useCallback适用于:
// 1. 传递给子组件的回调
function ParentWithCallback() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // 避免子组件重新渲染
return <ChildComponent onClick={handleClick} />;
}
// useEffectEvent适用于:
// 2. Effect中调用的非响应式逻辑
function ParentWithEffectEvent({ userId }) {
const [count, setCount] = useState(0);
const logActivity = useEffectEvent(() => {
analytics.log('user_activity', { userId, count });
});
useEffect(() => {
const timer = setInterval(() => {
logActivity(); // 不想因为count变化而重置定时器
}, 60000);
return () => clearInterval(timer);
}, [userId]); // 只依赖userId
}5.2 useEffectEvent vs useRef
对比ref模式:
jsx
// 使用ref(旧方案)
function WithRef({ value, onUpdate }) {
const onUpdateRef = useRef(onUpdate);
// 同步ref
useEffect(() => {
onUpdateRef.current = onUpdate;
}, [onUpdate]);
useEffect(() => {
const timer = setInterval(() => {
onUpdateRef.current(value);
}, 1000);
return () => clearInterval(timer);
}, [value]);
// 问题:
// 1. 需要额外的Effect同步ref
// 2. 两个useEffect,逻辑分散
// 3. 不够直观
}
// 使用useEffectEvent(新方案)
function WithEffectEvent({ value, onUpdate }) {
const update = useEffectEvent(() => {
onUpdate(value);
});
useEffect(() => {
const timer = setInterval(() => {
update();
}, 1000);
return () => clearInterval(timer);
}, [value]);
// 优势:
// 1. 一个Effect,逻辑集中
// 2. 不需要同步ref
// 3. 更清晰易懂
}5.3 useEffectEvent vs 拆分Effect
对比拆分Effect的方案:
jsx
// 拆分Effect(不推荐)
function WithSplitEffects({ roomId, theme, soundEnabled }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
useEffect(() => {
// 尝试在另一个Effect中处理通知
// 但无法知道连接何时建立
// 逻辑分离,难以维护
}, [theme, soundEnabled]);
// 问题:
// 1. 无法协调两个Effect
// 2. 逻辑分散
// 3. 时序问题
}
// 使用useEffectEvent(推荐)
function WithEffectEvent({ roomId, theme, soundEnabled }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
if (soundEnabled) {
playSound();
}
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
onConnected(); // 逻辑集中,时序明确
});
return () => connection.disconnect();
}, [roomId]);
// 优势:
// 1. 逻辑在一个Effect中
// 2. 时序清晰
// 3. 易于理解和维护
}第六部分:实战案例
6.1 实时聊天应用
构建聊天室功能:
jsx
function ChatRoom({
roomId,
currentUser,
theme,
soundEnabled,
onMessageReceived,
onUserJoined,
onUserLeft
}) {
const [messages, setMessages] = useState([]);
const [onlineUsers, setOnlineUsers] = useState([]);
// 所有回调都是非响应式的
const handleMessage = useEffectEvent((msg) => {
setMessages(prev => [...prev, msg]);
onMessageReceived(msg);
if (soundEnabled && msg.senderId !== currentUser.id) {
playMessageSound();
}
if (theme === 'dark') {
showDarkModeNotification(msg);
} else {
showLightModeNotification(msg);
}
});
const handleUserJoined = useEffectEvent((user) => {
setOnlineUsers(prev => [...prev, user]);
onUserJoined(user);
if (soundEnabled) {
playJoinSound();
}
});
const handleUserLeft = useEffectEvent((userId) => {
setOnlineUsers(prev => prev.filter(u => u.id !== userId));
onUserLeft(userId);
});
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
ws.onopen = () => {
console.log(`Connected to room: ${roomId}`);
ws.send(JSON.stringify({
type: 'join',
userId: currentUser.id,
username: currentUser.name
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'message':
handleMessage(data);
break;
case 'user_joined':
handleUserJoined(data.user);
break;
case 'user_left':
handleUserLeft(data.userId);
break;
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log(`Disconnected from room: ${roomId}`);
};
return () => {
ws.close();
};
}, [roomId, currentUser.id, currentUser.name]); // 只依赖连接相关的值
return (
<div className={`chat-room theme-${theme}`}>
<UserList users={onlineUsers} />
<MessageList messages={messages} />
<MessageInput roomId={roomId} />
</div>
);
}6.2 游戏逻辑
游戏中的事件处理:
jsx
function Game({ difficulty, soundEnabled, onScoreUpdate, onGameOver }) {
const [score, setScore] = useState(0);
const [level, setLevel] = useState(1);
const [gameState, setGameState] = useState('playing');
// 游戏事件是非响应式的
const handleScoreChange = useEffectEvent((points) => {
const newScore = score + points;
setScore(newScore);
onScoreUpdate(newScore);
if (soundEnabled) {
playScoreSound(points);
}
// 检查升级
if (newScore >= level * 100) {
setLevel(prev => prev + 1);
}
});
const handleGameEnd = useEffectEvent((reason) => {
setGameState('over');
onGameOver({
score,
level,
reason,
difficulty
});
if (soundEnabled) {
playGameOverSound();
}
});
useEffect(() => {
// 游戏循环
const gameLoop = setInterval(() => {
if (gameState !== 'playing') return;
// 游戏逻辑
const event = generateGameEvent();
if (event.type === 'score') {
handleScoreChange(event.points);
} else if (event.type === 'game_over') {
handleGameEnd(event.reason);
}
}, 1000 / (30 + difficulty * 10)); // FPS根据难度调整
return () => clearInterval(gameLoop);
}, [difficulty, gameState]); // 不包含score、soundEnabled等
return (
<div className="game">
<div className="score">Score: {score}</div>
<div className="level">Level: {level}</div>
<GameCanvas />
</div>
);
}6.3 数据可视化
图表更新和交互:
jsx
function InteractiveChart({
data,
options,
onDataPointClick,
onZoomChange,
theme
}) {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
// 交互回调是非响应式的
const handlePointClick = useEffectEvent((point) => {
onDataPointClick({
...point,
theme, // 使用当前theme
timestamp: Date.now()
});
});
const handleZoom = useEffectEvent((zoomLevel) => {
onZoomChange(zoomLevel);
});
// 初始化图表
useEffect(() => {
if (!chartRef.current) return;
const chart = createChart(chartRef.current, {
...options,
onClick: (point) => {
handlePointClick(point);
},
onZoom: (level) => {
handleZoom(level);
}
});
chartInstanceRef.current = chart;
return () => {
chart.destroy();
};
}, [options]); // 不包含回调函数
// 更新数据
useEffect(() => {
if (chartInstanceRef.current) {
chartInstanceRef.current.updateData(data);
}
}, [data]);
// 更新主题
useEffect(() => {
if (chartInstanceRef.current) {
chartInstanceRef.current.setTheme(theme);
}
}, [theme]);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
}第七部分:注意事项和限制
7.1 使用规则
useEffectEvent的使用规则:
jsx
// ✅ 正确:在Effect内部调用
function Correct({ value }) {
const handleEvent = useEffectEvent(() => {
console.log(value);
});
useEffect(() => {
handleEvent(); // 在Effect内调用
}, []);
}
// ❌ 错误:在事件处理器中调用
function Wrong({ value }) {
const handleEvent = useEffectEvent(() => {
console.log(value);
});
return (
<button onClick={() => handleEvent()}>
{/* 不能在事件处理器中调用 */}
</button>
);
}
// ❌ 错误:作为依赖传递
function Wrong({ value }) {
const handleEvent = useEffectEvent(() => {
console.log(value);
});
useEffect(() => {
// ...
}, [handleEvent]); // 不能作为依赖
}
// ❌ 错误:传递给其他组件
function Wrong({ value }) {
const handleEvent = useEffectEvent(() => {
console.log(value);
});
return (
<ChildComponent onEvent={handleEvent} />
{/* 不能传递给其他组件 */}
);
}正确的调用位置:
jsx
function CorrectUsage() {
const event1 = useEffectEvent(() => {});
const event2 = useEffectEvent(() => {});
useEffect(() => {
event1(); // ✅ 直接在Effect中调用
const handler = () => {
event2(); // ✅ 在Effect的回调中调用
};
element.addEventListener('click', handler);
return () => element.removeEventListener('click', handler);
}, []);
useLayoutEffect(() => {
event1(); // ✅ 在useLayoutEffect中调用
}, []);
useInsertionEffect(() => {
event1(); // ✅ 在useInsertionEffect中调用
}, []);
}7.2 实验性状态
useEffectEvent目前是实验性API:
当前状态(2025):
- 在React Canary版本中可用
- 在稳定版中需要手动polyfill
- API可能会变化
- 不建议在生产环境使用
未来计划:
- 预计在React 19正式版或后续版本中稳定
- API可能有微调
- 更多优化和改进
使用建议:
1. 实验性项目可以尝试
2. 生产环境谨慎使用
3. 关注React官方更新
4. 准备好适配API变化Polyfill实现:
jsx
// 简单的polyfill(仅用于理解,不要在生产环境使用)
function useEffectEvent(handler) {
const handlerRef = useRef(null);
// 同步最新handler到ref
useLayoutEffect(() => {
handlerRef.current = handler;
});
// 返回稳定的函数引用
return useCallback((...args) => {
const fn = handlerRef.current;
return fn(...args);
}, []);
}
// 注意:这只是简化版本
// 真实实现更复杂,包含:
// - 开发模式检查
// - 错误处理
// - 性能优化
// - 内存管理7.3 调试技巧
调试useEffectEvent:
jsx
function DebuggableComponent({ value, onChange }) {
const handleChange = useEffectEvent((newValue) => {
console.log('useEffectEvent called:', {
oldValue: value,
newValue,
timestamp: Date.now()
});
onChange(newValue);
});
useEffect(() => {
console.log('Effect running with value:', value);
const subscription = subscribe((data) => {
console.log('Subscription callback with data:', data);
handleChange(data);
});
return () => {
console.log('Effect cleanup');
subscription.unsubscribe();
};
}, [value]);
return <div>Value: {value}</div>;
}
// 输出分析:
// 1. value变化时:
// - "Effect cleanup"
// - "Effect running with value: new value"
//
// 2. onChange变化时:
// - 无输出(不触发Effect)
// - 但handleChange会使用新的onChange性能分析:
jsx
import { Profiler } from 'react';
function ProfiledComponent() {
const onRender = (id, phase, actualDuration) => {
console.log(`${id} ${phase}:`, actualDuration);
};
return (
<Profiler id="EffectEventTest" onRender={onRender}>
<ComponentWithEffectEvent />
</Profiler>
);
}第八部分:迁移指南
8.1 从useRef迁移
迁移步骤:
jsx
// 之前:使用ref
function Before({ callback }) {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
}, [callback]);
useEffect(() => {
const timer = setInterval(() => {
callbackRef.current();
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 之后:使用useEffectEvent
function After({ callback }) {
const handleTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const timer = setInterval(() => {
handleTick();
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 改进:
// - 删除同步ref的useEffect
// - 代码更简洁
// - 语义更清晰批量迁移:
jsx
// 迁移前
function OldComponent({ value1, value2, callback1, callback2 }) {
const callback1Ref = useRef(callback1);
const callback2Ref = useRef(callback2);
useEffect(() => {
callback1Ref.current = callback1;
}, [callback1]);
useEffect(() => {
callback2Ref.current = callback2;
}, [callback2]);
useEffect(() => {
const sub = subscribe(value1, value2, {
onUpdate: (data) => callback1Ref.current(data),
onError: (error) => callback2Ref.current(error)
});
return () => sub.unsubscribe();
}, [value1, value2]);
}
// 迁移后
function NewComponent({ value1, value2, callback1, callback2 }) {
const handleUpdate = useEffectEvent((data) => {
callback1(data);
});
const handleError = useEffectEvent((error) => {
callback2(error);
});
useEffect(() => {
const sub = subscribe(value1, value2, {
onUpdate: handleUpdate,
onError: handleError
});
return () => sub.unsubscribe();
}, [value1, value2]);
}
// 改进:
// - 从5个Hook减少到3个
// - 逻辑更集中
// - 性能更好8.2 从useCallback迁移
识别可以迁移的场景:
jsx
// 场景1:回调只在Effect中使用
// 之前
function Before({ value, onUpdate }) {
const handleUpdate = useCallback(() => {
onUpdate(value);
}, [value, onUpdate]);
useEffect(() => {
const timer = setInterval(handleUpdate, 1000);
return () => clearInterval(timer);
}, [handleUpdate]);
}
// 之后
function After({ value, onUpdate }) {
const handleUpdate = useEffectEvent(() => {
onUpdate(value);
});
useEffect(() => {
const timer = setInterval(handleUpdate, 1000);
return () => clearInterval(timer);
}, []);
}
// 场景2:回调传递给子组件(保持useCallback)
function KeepCallback({ onUpdate }) {
const handleUpdate = useCallback(() => {
onUpdate();
}, [onUpdate]); // 保持useCallback
return <ChildComponent onUpdate={handleUpdate} />;
}8.3 重构复杂Effect
逐步重构策略:
jsx
// 步骤1:识别非响应式逻辑
function Step1({ roomId, theme, soundEnabled, onMessage }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
showNotification('Connected!', theme); // 非响应式
});
connection.on('message', (msg) => {
onMessage(msg); // 非响应式
if (soundEnabled) { // 非响应式
playSound();
}
});
return () => connection.disconnect();
}, [roomId, theme, soundEnabled, onMessage]); // 依赖太多
}
// 步骤2:提取Effect Events
function Step2({ roomId, theme, soundEnabled, onMessage }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
const onMessageReceived = useEffectEvent((msg) => {
onMessage(msg);
if (soundEnabled) {
playSound();
}
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('connected', () => {
onConnected();
});
connection.on('message', (msg) => {
onMessageReceived(msg);
});
return () => connection.disconnect();
}, [roomId]); // 依赖大幅减少
}
// 步骤3:进一步优化(可选)
function Step3({ roomId, theme, soundEnabled, onMessage }) {
const handleConnectionEvents = useEffectEvent((connection) => {
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.on('message', (msg) => {
onMessage(msg);
if (soundEnabled) {
playSound();
}
});
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
handleConnectionEvents(connection);
return () => connection.disconnect();
}, [roomId]);
}第九部分:性能优化
9.1 减少Effect重新运行
性能对比:
jsx
// 不使用useEffectEvent:频繁重连
function Inefficient({ roomId, notificationCount }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('message', (msg) => {
// 使用notificationCount
showBadge(notificationCount + 1);
});
return () => connection.disconnect();
}, [roomId, notificationCount]); // notificationCount频繁变化
// 性能问题:
// - notificationCount每次消息都变化
// - 导致频繁断开重连
// - 用户体验差
}
// 使用useEffectEvent:稳定连接
function Efficient({ roomId, notificationCount }) {
const updateBadge = useEffectEvent(() => {
showBadge(notificationCount + 1);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
connection.on('message', (msg) => {
updateBadge(); // 使用最新notificationCount,但不触发重连
});
return () => connection.disconnect();
}, [roomId]); // 只在roomId变化时重连
// 优化效果:
// - 连接稳定
// - 总是使用最新值
// - 性能好
}性能测量:
jsx
function PerformanceComparison() {
const [reconnectCount, setReconnectCount] = useState(0);
const reconnectCountRef = useRef(0);
const trackReconnect = useEffectEvent(() => {
reconnectCountRef.current++;
setReconnectCount(reconnectCountRef.current);
});
useEffect(() => {
trackReconnect(); // 记录每次Effect运行
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [/* 依赖 */]);
return <div>Reconnections: {reconnectCount}</div>;
}9.2 内存优化
避免内存泄漏:
jsx
function MemoryEfficient({ items, onProcess }) {
const processItems = useEffectEvent((itemList) => {
// 处理大量数据
const processed = itemList.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));
onProcess(processed);
});
useEffect(() => {
// 防抖处理
const timer = setTimeout(() => {
processItems(items);
}, 500);
return () => clearTimeout(timer);
}, [items]); // items变化时重新处理
// 优势:
// - onProcess不在依赖中,避免闭包捕获大对象
// - 定时器正确清理
// - 内存使用优化
}9.3 批量操作
批量更新优化:
jsx
function BatchOperations({ operations, onComplete, onProgress }) {
const reportProgress = useEffectEvent((completed, total) => {
onProgress({
completed,
total,
percentage: (completed / total * 100).toFixed(1)
});
});
const reportComplete = useEffectEvent((results) => {
onComplete({
results,
duration: Date.now() - startTime,
successCount: results.filter(r => r.success).length
});
});
useEffect(() => {
let completed = 0;
const total = operations.length;
const results = [];
const startTime = Date.now();
const processNext = async () => {
if (completed >= total) {
reportComplete(results);
return;
}
const operation = operations[completed];
try {
const result = await executeOperation(operation);
results.push({ success: true, result });
} catch (error) {
results.push({ success: false, error });
}
completed++;
reportProgress(completed, total);
// 继续下一个
processNext();
};
processNext();
}, [operations]); // 只在operations变化时重新开始
return null;
}第十部分:常见模式
10.1 日志和监控
统一的日志模式:
jsx
function useLogger(componentName) {
const log = useEffectEvent((level, message, data) => {
const logEntry = {
component: componentName,
level,
message,
data,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// 发送到日志服务
sendToLoggingService(logEntry);
// 本地控制台
console[level](
`[${componentName}] ${message}`,
data
);
});
return log;
}
// 使用
function DataProcessor({ sourceId, processingConfig }) {
const log = useLogger('DataProcessor');
useEffect(() => {
log('info', 'Starting data processing', { sourceId });
const process = async () => {
try {
const data = await fetchData(sourceId);
log('info', 'Data fetched', { count: data.length });
const processed = processData(data, processingConfig);
log('info', 'Processing complete', {
inputCount: data.length,
outputCount: processed.length
});
} catch (error) {
log('error', 'Processing failed', {
error: error.message,
stack: error.stack
});
}
};
process();
}, [sourceId]); // processingConfig不触发重新处理
return <div>Processing...</div>;
}10.2 A/B测试
实验特性控制:
jsx
function useExperiment(experimentKey, variant, onExposure) {
const recordExposure = useEffectEvent(() => {
onExposure({
experimentKey,
variant,
userId: getCurrentUser().id,
timestamp: Date.now()
});
});
useEffect(() => {
// 只在实验variant变化时记录曝光
recordExposure();
}, [experimentKey, variant]);
}
// 使用
function FeatureComponent() {
const variant = useExperimentVariant('new-ui-test');
const [interactions, setInteractions] = useState(0);
useExperiment(
'new-ui-test',
variant,
(exposure) => {
// 回调可能包含频繁变化的state
analytics.recordExposure({
...exposure,
interactions, // 当前交互次数
sessionDuration: getSessionDuration()
});
}
);
return variant === 'control' ? <OldUI /> : <NewUI />;
}10.3 性能监控
监控Effect性能:
jsx
function usePerformanceMonitor(effectName, dependencies) {
const recordMetrics = useEffectEvent((duration, phase) => {
const metrics = {
effectName,
duration,
phase,
dependencies: JSON.stringify(dependencies),
timestamp: Date.now()
};
sendToMonitoring(metrics);
if (duration > 100) {
console.warn(`Slow Effect detected: ${effectName} took ${duration}ms`);
}
});
useEffect(() => {
const startTime = performance.now();
return () => {
const duration = performance.now() - startTime;
recordMetrics(duration, 'cleanup');
};
}, dependencies);
useEffect(() => {
const startTime = performance.now();
// Effect逻辑
const duration = performance.now() - startTime;
recordMetrics(duration, 'setup');
}, dependencies);
}
// 使用
function MonitoredComponent({ data }) {
usePerformanceMonitor('DataProcessing', [data]);
useEffect(() => {
// 被监控的Effect逻辑
processData(data);
}, [data]);
}第十一部分:测试策略
11.1 单元测试
测试使用useEffectEvent的组件:
jsx
import { renderHook, waitFor } from '@testing-library/react';
describe('useEffectEvent', () => {
test('calls handler with latest props', async () => {
const onUpdate = jest.fn();
let count = 0;
const { rerender } = renderHook(
({ value }) => {
const update = useEffectEvent(() => {
onUpdate(value);
});
useEffect(() => {
const timer = setTimeout(update, 100);
return () => clearTimeout(timer);
}, []);
},
{ initialProps: { value: count } }
);
// 等待Effect执行
await waitFor(() => {
expect(onUpdate).toHaveBeenCalledWith(0);
});
// 更新props
count = 1;
rerender({ value: count });
// Effect不会重新运行,但会使用新值
await new Promise(resolve => setTimeout(resolve, 150));
// 应该还是只调用一次(Effect没重新运行)
expect(onUpdate).toHaveBeenCalledTimes(1);
});
test('does not trigger effect re-run on handler change', () => {
const effectFn = jest.fn();
let handler = jest.fn();
const { rerender } = renderHook(
({ callback }) => {
const event = useEffectEvent(callback);
useEffect(() => {
effectFn();
return () => {};
}, []);
},
{ initialProps: { callback: handler } }
);
expect(effectFn).toHaveBeenCalledTimes(1);
// 更改handler
handler = jest.fn();
rerender({ callback: handler });
// Effect不应该重新运行
expect(effectFn).toHaveBeenCalledTimes(1);
});
});11.2 集成测试
测试完整流程:
jsx
import { render, screen, act } from '@testing-library/react';
test('chat room with useEffectEvent', async () => {
const mockConnection = {
connect: jest.fn(),
disconnect: jest.fn(),
on: jest.fn(),
off: jest.fn()
};
jest.spyOn(global, 'createConnection').mockReturnValue(mockConnection);
const onMessage = jest.fn();
const { rerender } = render(
<ChatRoom roomId="room1" theme="light" onMessage={onMessage} />
);
// 验证连接建立
expect(mockConnection.connect).toHaveBeenCalledTimes(1);
// 模拟接收消息
const messageHandler = mockConnection.on.mock.calls.find(
call => call[0] === 'message'
)[1];
act(() => {
messageHandler({ text: 'Hello', id: 1 });
});
expect(onMessage).toHaveBeenCalledWith({ text: 'Hello', id: 1 });
// 更改theme(不应重连)
rerender(<ChatRoom roomId="room1" theme="dark" onMessage={onMessage} />);
expect(mockConnection.disconnect).not.toHaveBeenCalled();
expect(mockConnection.connect).toHaveBeenCalledTimes(1); // 仍然是1次
// 更改roomId(应该重连)
rerender(<ChatRoom roomId="room2" theme="dark" onMessage={onMessage} />);
expect(mockConnection.disconnect).toHaveBeenCalledTimes(1);
expect(mockConnection.connect).toHaveBeenCalledTimes(2);
});常见问题
Q1: useEffectEvent什么时候稳定?
A: 目前是实验性API,预计在未来版本中稳定。
当前状态:
- React 18.x:不可用
- React 19 Canary:可用(实验性)
- React 19 Stable:待定
建议:
1. 学习概念和用法
2. 在实验项目中尝试
3. 关注React官方博客
4. 生产环境使用ref方案Q2: useEffectEvent能否在普通函数中调用?
A: 不能,只能在Effect中调用。
jsx
// ❌ 错误用法
function Wrong() {
const event = useEffectEvent(() => {});
const handleClick = () => {
event(); // 错误:不在Effect中
};
return <button onClick={handleClick}>Click</button>;
}
// ✅ 正确用法
function Correct() {
const event = useEffectEvent(() => {});
useEffect(() => {
const handleClick = () => {
event(); // 正确:在Effect内部
};
button.addEventListener('click', handleClick);
return () => button.removeEventListener('click', handleClick);
}, []);
}Q3: useEffectEvent和闭包陷阱
A: useEffectEvent专门用来避免闭包陷阱。
jsx
// 闭包陷阱示例
function ClosureProblem() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是0(闭包)
setCount(count + 1); // 永远设置为1
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖导致闭包
}
// useEffectEvent解决方案
function NoClosureProblem() {
const [count, setCount] = useState(0);
const increment = useEffectEvent(() => {
console.log(count); // 总是最新值
setCount(count + 1); // 正确递增
});
useEffect(() => {
const timer = setInterval(increment, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,但无闭包问题
}Q4: 能否嵌套使用useEffectEvent?
A: 可以,但要注意调用规则。
jsx
function Nested() {
const inner = useEffectEvent(() => {
console.log('Inner');
});
const outer = useEffectEvent(() => {
console.log('Outer');
inner(); // ✅ 可以调用另一个Effect Event
});
useEffect(() => {
outer();
}, []);
}Q5: useEffectEvent性能开销如何?
A: 非常小,几乎可以忽略。
性能分析:
jsx
// 性能测试
function PerformanceTest() {
const iterations = 10000;
// 测试useEffectEvent
const startEvent = performance.now();
for (let i = 0; i < iterations; i++) {
const event = useEffectEvent(() => {});
}
const eventTime = performance.now() - startEvent;
// 测试useCallback
const startCallback = performance.now();
for (let i = 0; i < iterations; i++) {
const callback = useCallback(() => {}, []);
}
const callbackTime = performance.now() - startCallback;
console.log('useEffectEvent:', eventTime, 'ms');
console.log('useCallback:', callbackTime, 'ms');
// 结果:性能相似,差异可忽略
}Q6: 如何调试useEffectEvent?
A: 使用console.log和React DevTools。
调试技巧:
jsx
function DebuggableEffect({ value }) {
const debugEvent = useEffectEvent((action) => {
console.group(`Effect Event: ${action}`);
console.log('Value:', value);
console.log('Stack:', new Error().stack);
console.groupEnd();
});
useEffect(() => {
console.log('Effect running');
debugEvent('setup');
return () => {
debugEvent('cleanup');
};
}, []);
}Q7: useEffectEvent能否替代所有useCallback?
A: 不能,它们的用途不同。
对比和选择:
jsx
// useCallback:用于组件间传递
function UseCallbackScenario() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<ExpensiveChild onClick={handleClick} />
{/* 避免子组件重新渲染 */}
);
}
// useEffectEvent:用于Effect内部
function UseEffectEventScenario({ value }) {
const logValue = useEffectEvent(() => {
console.log('value:', value);
});
useEffect(() => {
const timer = setInterval(() => {
logValue(); // 在Effect中调用
}, 1000);
return () => clearInterval(timer);
}, []);
}
// 选择指南:
// - 传递给子组件:useCallback
// - 在Effect中调用:useEffectEvent
// - 既传递又在Effect用:两者都需要Q8: useEffectEvent与StrictMode
A: StrictMode会双重调用,需要注意。
jsx
function StrictModeExample() {
const [count, setCount] = useState(0);
const increment = useEffectEvent(() => {
setCount(c => c + 1);
});
useEffect(() => {
console.log('Effect running');
increment();
return () => {
console.log('Effect cleanup');
};
}, []);
// StrictMode下输出:
// Effect running
// Effect cleanup
// Effect running
// count变化:
// 0 -> 1 -> 2 (因为Effect运行两次)
// 解决:使用函数式更新
const safeIncrement = useEffectEvent(() => {
setCount(c => c + 1); // ✅ 函数式更新,安全
});
}Q9: 如何处理异步Effect Event?
A: 可以是异步的,但要处理好清理。
jsx
function AsyncEffectEvent({ userId }) {
const [data, setData] = useState(null);
const fetchAndUpdate = useEffectEvent(async () => {
try {
const result = await fetchUserData(userId);
setData(result);
} catch (error) {
console.error('Fetch failed:', error);
}
});
useEffect(() => {
let cancelled = false;
const fetch = async () => {
if (cancelled) return;
await fetchAndUpdate();
};
fetch();
return () => {
cancelled = true;
};
}, [userId]);
}Q10: useEffectEvent能否访问DOM?
A: 可以,通过ref。
jsx
function DOMAccess() {
const ref = useRef(null);
const updateDOM = useEffectEvent(() => {
if (ref.current) {
ref.current.style.backgroundColor = getRandomColor();
}
});
useEffect(() => {
const timer = setInterval(() => {
updateDOM();
}, 1000);
return () => clearInterval(timer);
}, []);
return <div ref={ref}>Color changes every second</div>;
}总结
useEffectEvent实验性Hook的核心价值:
设计目标:
解决的问题:
1. Effect依赖困境
- 添加依赖:不必要的重新运行
- 不添加依赖:闭包陷阱
2. 非响应式值访问
- 需要最新值
- 但不想触发Effect
3. 代码可读性
- 清晰表达意图
- 减少hack和workaround核心特性:
1. 非响应式
✅ 不在依赖数组中
✅ 不触发Effect重新运行
✅ 总是访问最新值
2. 作用域限制
✅ 只能在Effect中调用
✅ 不能传递给其他组件
✅ 不能作为依赖
3. 性能优化
✅ 减少Effect运行
✅ 避免不必要的订阅/取消
✅ 更好的用户体验使用场景:
适合使用:
1. 日志和分析
2. 外部系统集成(WebSocket、定时器)
3. 动画和视觉效果
4. 性能监控
5. 非关键的回调
不适合使用:
1. 传递给子组件的回调(用useCallback)
2. 事件处理器(直接定义)
3. 需要响应式的逻辑最佳实践:
1. 明确区分响应式和非响应式值
2. 只在Effect中调用
3. 合理命名(表达非响应式意图)
4. 谨慎使用(实验性API)
5. 做好迁移准备
6. 完善的测试覆盖
7. 性能监控
8. 文档记录使用原因useEffectEvent是React团队对Effect依赖问题的优雅解决方案,虽然目前是实验性的,但展示了React未来的发展方向!