Appearance
useEffect副作用管理
学习目标
通过本章学习,你将全面掌握:
- useEffect的概念和作用机制
- 依赖数组的详细规则与最佳实践
- 清理函数的使用场景与模式
- 常见错误与解决方案详解
- 性能优化技巧与策略
- useEffect与React生命周期的对应关系
- React 19中useEffect的最新特性与最佳实践
- 复杂副作用管理的高级模式
- TypeScript中的useEffect类型定义
- 生产环境的调试与监控技巧
第一部分:useEffect核心概念
1.1 什么是副作用
在React中,副作用(Side Effect)是指与组件渲染输出无直接关系的操作。这些操作通常会与外部世界交互或产生可观察的效果。
jsx
import { useState, useEffect } from 'react';
// 副作用示例:修改文档标题
function DocumentTitleExample() {
const [count, setCount] = useState(0);
// 副作用:修改document.title(浏览器API)
useEffect(() => {
document.title = `你点击了 ${count} 次`;
});
return (
<button onClick={() => setCount(count + 1)}>
点击次数: {count}
</button>
);
}
// 常见的副作用类型
function SideEffectTypes() {
const [userId, setUserId] = useState(1);
// 1. 数据获取
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => console.log(data));
}, [userId]);
// 2. 订阅
useEffect(() => {
const subscription = subscribeToUserStatus(userId);
return () => subscription.unsubscribe();
}, [userId]);
// 3. 定时器
useEffect(() => {
const timer = setTimeout(() => {
console.log('延迟执行');
}, 1000);
return () => clearTimeout(timer);
}, []);
// 4. 手动DOM操作
useEffect(() => {
const element = document.getElementById('target');
element.style.color = 'red';
}, []);
// 5. 日志记录
useEffect(() => {
console.log('组件已挂载');
}, []);
// 6. 本地存储
useEffect(() => {
localStorage.setItem('userId', userId);
}, [userId]);
return <div>副作用示例</div>;
}1.2 useEffect基本语法
jsx
// useEffect完整语法
useEffect(
() => {
// 副作用代码(Effect函数)
return () => {
// 清理函数(Cleanup函数) - 可选
};
},
[dependencies] // 依赖数组 - 可选
);
// 基础示例
function BasicEffectExample() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Effect 1:每次渲染后执行
useEffect(() => {
console.log('组件渲染了');
});
// Effect 2:只在挂载时执行
useEffect(() => {
console.log('组件挂载了');
}, []);
// Effect 3:count变化时执行
useEffect(() => {
console.log('count变化为:', count);
}, [count]);
// Effect 4:带清理函数
useEffect(() => {
console.log('Effect执行');
return () => {
console.log('清理函数执行');
};
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>计数: {count}</button>
<input value={text} onChange={e => setText(e.target.value)} />
</div>
);
}1.3 执行时机详解
jsx
import { useEffect, useLayoutEffect } from 'react';
function ExecutionTimingDetail() {
const [count, setCount] = useState(0);
console.log('1. 组件渲染开始');
useLayoutEffect(() => {
console.log('3. useLayoutEffect执行(DOM更新后,浏览器绘制前)');
// 同步执行,会阻塞浏览器绘制
return () => {
console.log('useLayoutEffect清理');
};
});
useEffect(() => {
console.log('4. useEffect执行(浏览器绘制后)');
// 异步执行,不阻塞浏览器绘制
return () => {
console.log('useEffect清理');
};
});
console.log('2. 组件渲染结束');
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// 执行顺序总结
/*
首次渲染:
1. 组件渲染开始
2. 组件渲染结束
3. useLayoutEffect执行
4. 浏览器绘制
5. useEffect执行
更新渲染(count变化):
1. 组件渲染开始
2. 组件渲染结束
3. useLayoutEffect清理(旧的)
4. useLayoutEffect执行(新的)
5. 浏览器绘制
6. useEffect清理(旧的)
7. useEffect执行(新的)
组件卸载:
1. useLayoutEffect清理
2. useEffect清理
*/
// 时机对比示例
function TimingComparison() {
const [show, setShow] = useState(true);
return (
<div>
<button onClick={() => setShow(!show)}>切换</button>
{show && <TimingChild />}
</div>
);
}
function TimingChild() {
useEffect(() => {
console.log('useEffect:挂载');
return () => {
console.log('useEffect:卸载');
};
}, []);
useLayoutEffect(() => {
console.log('useLayoutEffect:挂载');
return () => {
console.log('useLayoutEffect:卸载');
};
}, []);
return <div>子组件</div>;
}第二部分:依赖数组深度解析
2.1 依赖数组的三种形式
jsx
function DependencyArrayForms() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 形式1:无依赖数组 - 每次渲染后都执行
useEffect(() => {
console.log('每次渲染都执行,包括首次渲染和所有更新');
// 用途:调试、性能追踪
});
// 形式2:空依赖数组 - 只在挂载时执行一次
useEffect(() => {
console.log('只在组件挂载时执行一次');
// 用途:初始化、一次性订阅、获取初始数据
return () => {
console.log('只在组件卸载时执行一次');
};
}, []);
// 形式3:有依赖数组 - 依赖变化时执行
useEffect(() => {
console.log('count或text变化时执行');
// 用途:响应特定状态/props的变化
}, [count, text]);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<input value={text} onChange={e => setText(e.target.value)} />
</div>
);
}
// 依赖数组的对比
function DependencyComparison() {
const [count, setCount] = useState(0);
// 无依赖数组:过度执行
useEffect(() => {
console.log('过度执行:每次渲染都获取数据');
fetchData();
}); // ❌ 每次渲染都执行,性能问题
// 空依赖数组:只执行一次
useEffect(() => {
console.log('只执行一次:初始化获取数据');
fetchData();
}, []); // ✅ 只在挂载时执行
// 有依赖数组:按需执行
useEffect(() => {
console.log('按需执行:count变化时获取数据');
fetchData(count);
}, [count]); // ✅ count变化时执行
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}2.2 依赖数组的规则
jsx
import { useState, useEffect, useCallback, useMemo } from 'react';
function DependencyRules() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: 'Alice' });
// 规则1:必须列出Effect中使用的所有响应式值
useEffect(() => {
console.log('count:', count);
console.log('user:', user.name);
}, [count, user]); // 必须包含count和user
// 规则2:函数也是依赖
const handleLog = () => {
console.log('count:', count);
};
useEffect(() => {
handleLog();
}, [handleLog]); // handleLog每次渲染都是新函数,导致Effect频繁执行
// 规则3:对象和数组每次渲染都是新的引用
const config = { threshold: count };
useEffect(() => {
processConfig(config);
}, [config]); // config每次都是新对象,导致Effect频繁执行
return <div>{count}</div>;
}
// 依赖优化
function DependencyOptimization() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: 'Alice' });
// 优化1:使用useCallback稳定函数引用
const handleLog = useCallback(() => {
console.log('count:', count);
}, [count]); // 只在count变化时重新创建函数
useEffect(() => {
handleLog();
}, [handleLog]); // handleLog引用稳定
// 优化2:使用useMemo稳定对象引用
const config = useMemo(() => ({
threshold: count,
userId: user.id
}), [count, user.id]); // 只在count或user.id变化时重新创建对象
useEffect(() => {
processConfig(config);
}, [config]); // config引用稳定
// 优化3:直接在Effect中创建对象
useEffect(() => {
const config = { threshold: count, userId: user.id };
processConfig(config);
}, [count, user.id]); // 直接依赖原始值,无需useMemo
return <div>{count}</div>;
}
// 依赖检查
function DependencyLinting() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// ❌ ESLint警告:React Hook useEffect has a missing dependency: 'text'
useEffect(() => {
console.log(count, text);
}, [count]); // 遗漏text依赖
// ✅ 修复:添加所有依赖
useEffect(() => {
console.log(count, text);
}, [count, text]);
// ❌ ESLint警告:React Hook useEffect has a complex expression in the dependency array
useEffect(() => {
console.log(user.name);
}, [user.name]); // 应该依赖整个user对象
// ✅ 修复:依赖整个对象或提取变量
const userName = user.name;
useEffect(() => {
console.log(userName);
}, [userName]);
return <div>{count}</div>;
}2.3 依赖数组的陷阱
jsx
function DependencyTraps() {
const [count, setCount] = useState(0);
// 陷阱1:遗漏依赖导致闭包陷阱
useEffect(() => {
const timer = setInterval(() => {
console.log('count:', count); // count永远是0
}, 1000);
return () => clearInterval(timer);
}, []); // ❌ 遗漏count依赖
// 解决:添加依赖或使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => {
console.log('count:', prev); // 总是最新值
return prev;
});
}, 1000);
return () => clearInterval(timer);
}, []); // ✅ 使用函数式更新,无需依赖count
// 陷阱2:对象依赖导致无限循环
const [data, setData] = useState({ items: [] });
useEffect(() => {
fetchData().then(result => {
setData({ items: result }); // 创建新对象
});
}, [data]); // ❌ data变化触发Effect,Effect更新data,无限循环
// 解决:依赖对象的特定属性或使用其他状态触发
useEffect(() => {
fetchData().then(result => {
setData({ items: result });
});
}, [data.items.length]); // ✅ 只依赖长度
// 或使用单独的触发器
const [trigger, setTrigger] = useState(0);
useEffect(() => {
fetchData().then(result => {
setData({ items: result });
});
}, [trigger]); // ✅ 依赖触发器,手动控制
return <div>{count}</div>;
}
// Props作为依赖
function PropsAsDependency({ userId, onUserChange, config }) {
const [user, setUser] = useState(null);
// 问题:props每次可能都是新的
useEffect(() => {
fetchUser(userId).then(user => {
setUser(user);
onUserChange(user); // 回调函数每次都是新的
});
}, [userId, onUserChange]); // 如果onUserChange每次都是新的,Effect会频繁执行
// 解决:父组件使用useCallback
// 在Parent组件中:
// const handleUserChange = useCallback((user) => {
// console.log(user);
// }, []);
return <div>{user?.name}</div>;
}第三部分:清理函数详解
3.1 清理函数的作用
jsx
function CleanupFunctionRole() {
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
useEffect(() => {
console.log('Effect执行,设置副作用');
// 设置定时器
const timer = setInterval(() => {
console.log('定时器执行');
}, 1000);
// 清理函数:移除副作用
return () => {
clearInterval(timer);
console.log('清理函数执行,移除定时器');
};
}, [count]); // count变化时,先清理旧Effect,再执行新Effect
// 生命周期说明:
// 1. 组件挂载 → Effect执行
// 2. count变化 → 清理函数执行(清理旧Effect) → Effect执行(设置新Effect)
// 3. 组件卸载 → 清理函数执行(清理最后的Effect)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
<button onClick={() => setShow(false)}>卸载组件</button>
</div>
);
}
// 清理函数的必要性
function WhyCleanup() {
const [subscribed, setSubscribed] = useState(true);
// ❌ 没有清理函数
useEffect(() => {
const subscription = subscribeToUpdates();
// 组件卸载时订阅未取消,导致内存泄漏
}, []);
// ✅ 有清理函数
useEffect(() => {
const subscription = subscribeToUpdates();
return () => {
subscription.unsubscribe(); // 清理订阅,防止内存泄漏
};
}, []);
return <div>订阅状态: {subscribed ? '已订阅' : '未订阅'}</div>;
}3.2 清理函数的场景
jsx
function CleanupScenarios() {
const [activeTab, setActiveTab] = useState('home');
// 场景1:清理定时器
useEffect(() => {
const timer = setTimeout(() => {
console.log('延迟执行');
}, 1000);
return () => {
clearTimeout(timer);
console.log('清理定时器');
};
}, [activeTab]);
// 场景2:清理间隔定时器
useEffect(() => {
const interval = setInterval(() => {
console.log('周期执行');
}, 1000);
return () => {
clearInterval(interval);
console.log('清理间隔定时器');
};
}, []);
// 场景3:清理事件监听
useEffect(() => {
const handleResize = () => {
console.log('窗口大小:', window.innerWidth, window.innerHeight);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
console.log('清理事件监听');
};
}, []);
// 场景4:取消网络请求
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', {
signal: controller.signal
})
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => {
controller.abort();
console.log('取消网络请求');
};
}, [activeTab]);
// 场景5:清理订阅
useEffect(() => {
const subscription = subscribeToMessages((message) => {
console.log('收到消息:', message);
});
return () => {
subscription.unsubscribe();
console.log('清理订阅');
};
}, []);
// 场景6:清理DOM操作
useEffect(() => {
const element = document.getElementById('target');
element.classList.add('active');
return () => {
element.classList.remove('active');
console.log('清理DOM操作');
};
}, []);
// 场景7:清理动画
useEffect(() => {
const element = document.getElementById('animated');
const animation = element.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(100px)' }
], {
duration: 1000,
iterations: Infinity
});
return () => {
animation.cancel();
console.log('取消动画');
};
}, []);
return (
<div>
<button onClick={() => setActiveTab('home')}>Home</button>
<button onClick={() => setActiveTab('profile')}>Profile</button>
</div>
);
}
// WebSocket清理示例
function WebSocketCleanup({ roomId }) {
const [messages, setMessages] = useState([]);
const [connected, setConnected] = useState(false);
useEffect(() => {
// 创建WebSocket连接
const ws = new WebSocket(`wss://example.com/rooms/${roomId}`);
ws.onopen = () => {
console.log('WebSocket连接已建立');
setConnected(true);
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
setConnected(false);
};
ws.onclose = () => {
console.log('WebSocket连接已关闭');
setConnected(false);
};
// 清理:关闭WebSocket连接
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
console.log('清理:关闭WebSocket连接');
}
};
}, [roomId]); // roomId变化时,关闭旧连接,建立新连接
return (
<div>
<div className={`status ${connected ? 'connected' : 'disconnected'}`}>
状态: {connected ? '已连接' : '未连接'}
</div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
</div>
);
}3.3 清理函数的时机
jsx
function CleanupTiming() {
const [count, setCount] = useState(0);
const [show, setShow] = useState(true);
useEffect(() => {
console.log(`Effect执行: count = ${count}`);
return () => {
// 注意:这里的count是闭包中的值,是Effect执行时的count
console.log(`清理函数执行: count = ${count}`);
};
}, [count]);
// 执行顺序演示:
// 初始渲染(count=0):
// → Effect执行: count = 0
//
// 点击按钮(count变为1):
// → 清理函数执行: count = 0 (旧的闭包值)
// → Effect执行: count = 1
//
// 再次点击(count变为2):
// → 清理函数执行: count = 1 (旧的闭包值)
// → Effect执行: count = 2
//
// 组件卸载:
// → 清理函数执行: count = 2 (最后的闭包值)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
<button onClick={() => setShow(false)}>卸载</button>
</div>
);
}
// 清理函数与异步操作
function CleanupWithAsync() {
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
console.log('开始获取用户数据:', userId);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) { // 检查是否已取消
setUser(data);
console.log('成功设置用户数据:', userId);
} else {
console.log('已取消,不设置用户数据:', userId);
}
});
return () => {
cancelled = true; // 标记为已取消
console.log('清理函数:标记请求已取消:', userId);
};
}, [userId]);
// 快速切换userId时:
// 1. userId=1 → 开始获取用户1
// 2. userId=2 → 清理函数执行(cancelled=true for user1) → 开始获取用户2
// 3. 用户1请求返回 → 由于cancelled=true,不设置状态
// 4. 用户2请求返回 → cancelled=false,设置状态
return (
<div>
<button onClick={() => setUserId(1)}>用户1</button>
<button onClick={() => setUserId(2)}>用户2</button>
<div>{user?.name}</div>
</div>
);
}第四部分:常见错误与解决方案
4.1 闭包陷阱
jsx
function ClosureTrap() {
const [count, setCount] = useState(0);
// ❌ 问题:闭包导致count永远是初始值
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count); // count永远是0
setCount(count + 1); // 基于0递增,count只能变成1
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖导致闭包
return <div>{count}</div>;
}
// 解决方案汇总
function ClosureSolutions() {
const [count, setCount] = useState(0);
// 方案1:添加依赖(会导致定时器重建)
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', count);
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // 每次count变化都重建定时器
// 方案2:使用函数式更新(推荐)
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => {
console.log('Count:', prev);
return prev + 1; // 基于最新值
});
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖即可
// 方案3:使用useRef
const countRef = useRef(count);
useEffect(() => {
countRef.current = count; // 同步最新值到ref
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
console.log('Count:', countRef.current);
setCount(countRef.current + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
// 方案4:使用useReducer
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
}, { count: 0 });
useEffect(() => {
const timer = setInterval(() => {
dispatch({ type: 'increment' }); // dispatch总是稳定的
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖即可
return <div>{count}</div>;
}4.2 无限循环
jsx
function InfiniteLoopErrors() {
const [data, setData] = useState([]);
// ❌ 错误1:依赖导致的无限循环
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData); // 更新data
}, [data]); // data变化触发Effect,Effect更新data,无限循环
// ❌ 错误2:对象依赖导致的无限循环
const [user, setUser] = useState({ id: 1 });
useEffect(() => {
setUser({ ...user, name: 'Alice' }); // 创建新对象
}, [user]); // user引用变化,无限循环
// ❌ 错误3:Effect内部调用setState
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // 每次渲染都执行,无限循环
});
return <div>{data.length}</div>;
}
// 解决方案
function InfiniteLoopFixes() {
// 解决1:移除不必要的依赖
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // 只在挂载时执行
// 解决2:依赖对象的特定属性
const [user, setUser] = useState({ id: 1, name: '' });
useEffect(() => {
if (!user.name) {
fetch(`/api/users/${user.id}`)
.then(res => res.json())
.then(data => setUser({ ...user, name: data.name }));
}
}, [user.id]); // 只依赖id,不依赖整个user对象
// 解决3:使用条件判断
const [count, setCount] = useState(0);
useEffect(() => {
if (count < 10) { // 添加条件
setCount(count + 1);
}
}, [count]);
// 解决4:使用useMemo缓存对象
const config = useMemo(() => ({
threshold: 100,
userId: user.id
}), [user.id]);
useEffect(() => {
processConfig(config);
}, [config]); // config引用稳定
return <div>{count}</div>;
}4.3 竞态条件
jsx
function RaceConditionError({ userId }) {
const [user, setUser] = useState(null);
// ❌ 问题:快速切换导致数据错乱
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser);
// 问题场景:
// 1. userId=1,发起请求A
// 2. userId=2,发起请求B
// 3. 请求B先返回,设置user=2
// 4. 请求A后返回,设置user=1
// 结果:userId=2,但显示的是user=1的数据
}, [userId]);
return <div>{user?.name}</div>;
}
// 解决方案
function RaceConditionFixes({ userId }) {
const [user, setUser] = useState(null);
// 方案1:使用ignore标记
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!ignore) { // 检查是否已过期
setUser(data);
}
});
return () => {
ignore = true; // 清理时标记为已过期
};
}, [userId]);
// 方案2:使用AbortController
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, {
signal: controller.signal
})
.then(res => res.json())
.then(setUser)
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => {
controller.abort(); // 取消请求
};
}, [userId]);
// 方案3:使用版本号
const versionRef = useRef(0);
useEffect(() => {
const currentVersion = ++versionRef.current;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (versionRef.current === currentVersion) { // 检查版本
setUser(data);
}
});
}, [userId]);
return <div>{user?.name}</div>;
}4.4 内存泄漏
jsx
function MemoryLeakErrors() {
const [count, setCount] = useState(0);
// ❌ 错误1:未清理定时器
useEffect(() => {
setInterval(() => {
console.log('定时器执行');
}, 1000);
// 组件卸载后定时器仍在运行,内存泄漏
}, []);
// ❌ 错误2:未取消订阅
useEffect(() => {
const subscription = subscribeToUpdates();
// 组件卸载后订阅仍然存在,内存泄漏
}, []);
// ❌ 错误3:未移除事件监听
useEffect(() => {
window.addEventListener('resize', handleResize);
// 组件卸载后监听器仍然存在,内存泄漏
}, []);
// ❌ 错误4:异步操作后设置状态
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
// 组件卸载后请求返回,尝试设置已卸载组件的状态,内存泄漏
}, []);
return <div>{count}</div>;
}
// 解决方案
function MemoryLeakFixes() {
// 解决1:清理定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行');
}, 1000);
return () => clearInterval(timer); // ✅ 清理定时器
}, []);
// 解决2:取消订阅
useEffect(() => {
const subscription = subscribeToUpdates();
return () => subscription.unsubscribe(); // ✅ 取消订阅
}, []);
// 解决3:移除事件监听
useEffect(() => {
const handleResize = () => {
console.log('窗口大小变化');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize); // ✅ 移除监听
};
}, []);
// 解决4:检查组件是否已卸载
useEffect(() => {
let cancelled = false;
fetch('/api/data')
.then(res => res.json())
.then(data => {
if (!cancelled) { // ✅ 检查是否已卸载
setData(data);
}
});
return () => {
cancelled = true;
};
}, []);
return <div>内容</div>;
}第五部分:实战案例
5.1 完整的数据获取
jsx
function DataFetchingComplete({ url, options = {} }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
setLoading(false);
}
} catch (err) {
if (!cancelled && err.name !== 'AbortError') {
setError(err.message);
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
controller.abort();
};
}, [url, JSON.stringify(options)]);
return { data, loading, error };
}
// 使用示例
function UserProfile({ userId }) {
const { data: user, loading, error } = DataFetchingComplete({
url: `/api/users/${userId}`
});
if (loading) return <div className="loading">加载中...</div>;
if (error) return <div className="error">错误: {error}</div>;
if (!user) return null;
return (
<div className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}5.2 窗口事件监听
jsx
function WindowEventListeners() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
const [scrollPosition, setScrollPosition] = useState({
x: window.scrollX,
y: window.scrollY
});
const [isOnline, setIsOnline] = useState(navigator.onLine);
// 监听窗口大小变化
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
// 监听滚动位置
useEffect(() => {
const handleScroll = () => {
setScrollPosition({
x: window.scrollX,
y: window.scrollY
});
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// 监听网络状态
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return (
<div className="window-info">
<div>窗口大小: {windowSize.width} x {windowSize.height}</div>
<div>滚动位置: X={scrollPosition.x}, Y={scrollPosition.y}</div>
<div>网络状态: {isOnline ? '在线' : '离线'}</div>
</div>
);
}
// 防抖版本(性能优化)
function WindowEventListenersDebounced() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
let timeoutId;
const handleResize = () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}, 200); // 200ms防抖
};
window.addEventListener('resize', handleResize);
return () => {
clearTimeout(timeoutId);
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<div>窗口大小: {windowSize.width} x {windowSize.height}</div>
);
}5.3 LocalStorage同步
jsx
function LocalStorageSync({ storageKey, initialValue }) {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(storageKey);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('读取localStorage失败:', error);
return initialValue;
}
});
// 同步到localStorage
useEffect(() => {
try {
localStorage.setItem(storageKey, JSON.stringify(value));
} catch (error) {
console.error('写入localStorage失败:', error);
}
}, [storageKey, value]);
// 监听其他标签页的变化
useEffect(() => {
const handleStorageChange = (e) => {
if (e.key === storageKey && e.newValue !== null) {
try {
setValue(JSON.parse(e.newValue));
} catch (error) {
console.error('解析localStorage变化失败:', error);
}
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [storageKey]);
return [value, setValue];
}
// 使用示例
function ThemeSwitcher() {
const [theme, setTheme] = LocalStorageSync('theme', 'light');
useEffect(() => {
document.body.className = theme;
return () => {
document.body.className = '';
};
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
切换主题: {theme}
</button>
);
}5.4 Intersection Observer
jsx
function LazyLoadImage({ src, alt, placeholder }) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !isLoaded) {
setImageSrc(src);
setIsLoaded(true);
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.1,
rootMargin: '50px'
}
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
if (imgRef.current) {
observer.unobserve(imgRef.current);
}
};
}, [src, isLoaded]);
return (
<img
ref={imgRef}
src={imageSrc}
alt={alt}
className={isLoaded ? 'loaded' : 'loading'}
/>
);
}
// 无限滚动
function InfiniteScroll({ loadMore, hasMore }) {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const loadMoreRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
async (entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
setLoading(true);
try {
const newItems = await loadMore();
setItems(prev => [...prev, ...newItems]);
} catch (error) {
console.error('加载更多失败:', error);
} finally {
setLoading(false);
}
}
},
{
threshold: 1.0
}
);
if (loadMoreRef.current) {
observer.observe(loadMoreRef.current);
}
return () => {
if (loadMoreRef.current) {
observer.unobserve(loadMoreRef.current);
}
};
}, [hasMore, loading, loadMore]);
return (
<div className="infinite-scroll">
{items.map(item => (
<div key={item.id} className="item">{item.text}</div>
))}
{hasMore && (
<div ref={loadMoreRef} className="load-more">
{loading ? '加载中...' : '加载更多'}
</div>
)}
{!hasMore && <div className="no-more">没有更多了</div>}
</div>
);
}第六部分:性能优化
6.1 减少Effect执行频率
jsx
// 防抖Hook
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
// 节流Hook
function useThrottle(value, limit) {
const [throttledValue, setThrottledValue] = useState(value);
const lastRan = useRef(Date.now());
useEffect(() => {
const handler = setTimeout(() => {
if (Date.now() - lastRan.current >= limit) {
setThrottledValue(value);
lastRan.current = Date.now();
}
}, limit - (Date.now() - lastRan.current));
return () => {
clearTimeout(handler);
};
}, [value, limit]);
return throttledValue;
}
// 使用示例
function SearchWithDebounce() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const debouncedSearchTerm = useDebounce(searchTerm, 500);
// 只在防抖值变化时搜索
useEffect(() => {
if (debouncedSearchTerm) {
fetch(`/api/search?q=${debouncedSearchTerm}`)
.then(res => res.json())
.then(setResults);
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
function ScrollWithThrottle() {
const [scrollY, setScrollY] = useState(0);
const throttledScrollY = useThrottle(scrollY, 200);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
// 只在节流值变化时执行昂贵操作
useEffect(() => {
console.log('执行昂贵操作:', throttledScrollY);
}, [throttledScrollY]);
return <div>滚动位置: {throttledScrollY}</div>;
}6.2 条件执行Effect
jsx
function ConditionalEffect({ shouldFetch, userId }) {
const [data, setData] = useState(null);
// ✅ 好:条件判断在Effect内部
useEffect(() => {
if (!shouldFetch) return;
let cancelled = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setData(data);
}
});
return () => {
cancelled = true;
};
}, [shouldFetch, userId]);
return <div>{data?.name}</div>;
}
// 延迟执行Effect
function DelayedEffect() {
const [show, setShow] = useState(false);
// 挂载后延迟执行
useEffect(() => {
const timer = setTimeout(() => {
setShow(true);
}, 1000);
return () => clearTimeout(timer);
}, []);
// 只在show为true时执行
useEffect(() => {
if (!show) return;
console.log('延迟执行的Effect');
loadHeavyResource();
}, [show]);
return <div>{show && '内容已显示'}</div>;
}6.3 Effect拆分策略
jsx
// ❌ 不好:一个大Effect,任何依赖变化都重新执行所有逻辑
function BadSingleEffect({ userId, theme, language }) {
useEffect(() => {
// 获取用户数据
fetchUser(userId);
// 订阅更新
const unsubscribe = subscribeToUpdates(userId);
// 应用主题
document.body.className = theme;
// 加载翻译
loadTranslations(language);
return () => {
unsubscribe();
document.body.className = '';
};
}, [userId, theme, language]); // 任何变化都重新执行所有逻辑
return <div>内容</div>;
}
// ✅ 好:按功能拆分为多个Effect
function GoodMultipleEffects({ userId, theme, language }) {
// Effect 1:获取用户数据和订阅(依赖userId)
useEffect(() => {
fetchUser(userId);
const unsubscribe = subscribeToUpdates(userId);
return () => {
unsubscribe();
};
}, [userId]);
// Effect 2:应用主题(依赖theme)
useEffect(() => {
document.body.className = theme;
return () => {
document.body.className = '';
};
}, [theme]);
// Effect 3:加载翻译(依赖language)
useEffect(() => {
loadTranslations(language);
}, [language]);
return <div>内容</div>;
}
// 按职责拆分为自定义Hooks
function WithCustomHooks({ userId, theme, language }) {
useUserData(userId);
useTheme(theme);
useTranslation(language);
return <div>内容</div>;
}
function useUserData(userId) {
useEffect(() => {
fetchUser(userId);
const unsubscribe = subscribeToUpdates(userId);
return () => {
unsubscribe();
};
}, [userId]);
}
function useTheme(theme) {
useEffect(() => {
document.body.className = theme;
return () => {
document.body.className = '';
};
}, [theme]);
}
function useTranslation(language) {
useEffect(() => {
loadTranslations(language);
}, [language]);
}第七部分:React 19最佳实践
7.1 使用use()替代useEffect
jsx
import { use, Suspense } from 'react';
// 旧方式:useEffect + useState
function OldWay({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(user => {
setUser(user);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
return <div>{user?.name}</div>;
}
// 新方式:use() (React 19)
function fetchUser(userId) {
return fetch(`/api/users/${userId}`)
.then(res => res.json());
}
function NewWay({ userId }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
// 包裹在Suspense中
function App() {
const [userId, setUserId] = useState(1);
return (
<div>
<button onClick={() => setUserId(1)}>用户1</button>
<button onClick={() => setUserId(2)}>用户2</button>
<Suspense fallback={<div>加载中...</div>}>
<ErrorBoundary fallback={<div>加载失败</div>}>
<NewWay userId={userId} />
</ErrorBoundary>
</Suspense>
</div>
);
}7.2 Server Actions集成
jsx
'use client';
import { useActionState, useOptimistic } from 'react';
// Server Action
async function createTodo(prevState, formData) {
'use server';
const text = formData.get('text');
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
const newTodo = {
id: Date.now(),
text,
completed: false
};
return { success: true, todo: newTodo };
}
// 使用useActionState
function TodoForm() {
const [state, formAction, isPending] = useActionState(createTodo, null);
return (
<form action={formAction}>
<input name="text" required />
<button disabled={isPending}>
{isPending ? '添加中...' : '添加'}
</button>
{state?.success && (
<div className="success">添加成功: {state.todo.text}</div>
)}
</form>
);
}
// 使用useOptimistic实现乐观更新
function TodoListOptimistic() {
const [todos, setTodos] = useState([]);
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo]
);
const handleSubmit = async (formData) => {
const text = formData.get('text');
const optimisticTodo = {
id: Date.now(),
text,
completed: false,
pending: true
};
// 立即显示乐观更新
addOptimisticTodo(optimisticTodo);
// 实际创建
const result = await createTodo(null, formData);
if (result.success) {
setTodos([...todos, result.todo]);
}
};
return (
<div>
<form action={handleSubmit}>
<input name="text" required />
<button>添加</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} className={todo.pending ? 'pending' : ''}>
{todo.text}
</li>
))}
</ul>
</div>
);
}7.3 Transition集成
jsx
import { useTransition, useDeferredValue } from 'react';
function SearchWithTransition() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
// 不使用useEffect,而是在事件处理中使用transition
const handleSearch = (value) => {
setSearchTerm(value);
startTransition(() => {
// 这是一个低优先级更新
fetch(`/api/search?q=${value}`)
.then(res => res.json())
.then(setResults);
});
};
return (
<div>
<input
value={searchTerm}
onChange={e => handleSearch(e.target.value)}
/>
{isPending && <div>搜索中...</div>}
<ul>
{results.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
</div>
);
}
function FilteredListWithDeferred({ items, filter }) {
const deferredFilter = useDeferredValue(filter);
// 使用延迟值过滤,不会阻塞输入
const filteredItems = useMemo(() => {
return items.filter(item =>
item.text.toLowerCase().includes(deferredFilter.toLowerCase())
);
}, [items, deferredFilter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
);
}第八部分:调试与监控
8.1 Effect执行追踪
jsx
function useEffectDebugger(effect, dependencies, debugName = 'Effect') {
const previousDeps = useRef(dependencies);
const renderCount = useRef(0);
renderCount.current++;
useEffect(() => {
const changedDeps = dependencies.reduce((acc, dep, index) => {
if (dep !== previousDeps.current[index]) {
acc.push({
index,
previous: previousDeps.current[index],
current: dep
});
}
return acc;
}, []);
if (changedDeps.length > 0) {
console.group(`[${debugName}] Effect执行 (渲染 #${renderCount.current})`);
console.log('变化的依赖:', changedDeps);
console.log('所有依赖:', dependencies);
console.groupEnd();
}
previousDeps.current = dependencies;
const cleanup = effect();
return () => {
console.log(`[${debugName}] 清理函数执行`);
if (cleanup) cleanup();
};
}, dependencies);
}
// 使用
function DebugComponent({ userId }) {
const [count, setCount] = useState(0);
useEffectDebugger(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(console.log);
}, [userId, count], '用户数据获取');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>计数: {count}</button>
</div>
);
}8.2 性能监控
jsx
function useEffectPerformance(effect, dependencies, name = 'Effect') {
const executionCount = useRef(0);
const totalDuration = useRef(0);
const lastExecutionTime = useRef(0);
useEffect(() => {
const startTime = performance.now();
const cleanup = effect();
const endTime = performance.now();
const duration = endTime - startTime;
executionCount.current++;
totalDuration.current += duration;
lastExecutionTime.current = duration;
console.log(`[${name}] 性能分析:`, {
'本次耗时': `${duration.toFixed(2)}ms`,
'执行次数': executionCount.current,
'平均耗时': `${(totalDuration.current / executionCount.current).toFixed(2)}ms`,
'总耗时': `${totalDuration.current.toFixed(2)}ms`
});
// 性能警告
if (duration > 100) {
console.warn(`[${name}] 性能警告: Effect执行时间过长 (${duration.toFixed(2)}ms)`);
}
return cleanup;
}, dependencies);
// 返回性能指标
return {
executionCount: executionCount.current,
averageDuration: totalDuration.current / executionCount.current,
lastDuration: lastExecutionTime.current
};
}
// 使用
function PerformanceMonitoring() {
const [data, setData] = useState([]);
const metrics = useEffectPerformance(() => {
const processed = expensiveOperation(data);
console.log('处理结果:', processed);
}, [data], '数据处理');
return (
<div>
<div>执行次数: {metrics.executionCount}</div>
<div>平均耗时: {metrics.averageDuration?.toFixed(2)}ms</div>
<div>上次耗时: {metrics.lastDuration?.toFixed(2)}ms</div>
</div>
);
}8.3 Effect可视化
jsx
function useEffectLogger(name) {
const renderCount = useRef(0);
const effectCount = useRef(0);
renderCount.current++;
console.log(`[${name}] 渲染 #${renderCount.current}`);
useEffect(() => {
effectCount.current++;
console.log(`[${name}] Effect执行 #${effectCount.current}`);
return () => {
console.log(`[${name}] 清理 #${effectCount.current}`);
};
});
}
function LoggedComponent({ userId }) {
useEffectLogger('LoggedComponent');
const [count, setCount] = useState(0);
useEffect(() => {
console.log('userId Effect:', userId);
}, [userId]);
useEffect(() => {
console.log('count Effect:', count);
}, [count]);
return (
<div>
<div>userId: {userId}</div>
<button onClick={() => setCount(c => c + 1)}>count: {count}</button>
</div>
);
}第九部分:TypeScript支持
9.1 Effect的类型定义
typescript
import { useEffect, DependencyList, EffectCallback } from 'react';
// 基本类型
function TypedEffect() {
const [count, setCount] = useState<number>(0);
// Effect函数类型: EffectCallback = () => void | Destructor
useEffect(() => {
console.log(count);
// 返回清理函数: Destructor = () => void
return () => {
console.log('清理');
};
}, [count]); // 依赖数组类型: DependencyList = ReadonlyArray<any>
}
// 自定义Effect Hook
function useCustomEffect(
effect: EffectCallback,
deps: DependencyList,
debugName: string = 'Effect'
) {
useEffect(() => {
console.log(`[${debugName}] 执行`);
const cleanup = effect();
return () => {
console.log(`[${debugName}] 清理`);
if (cleanup) cleanup();
};
}, deps);
}
// 异步Effect的类型
function useAsyncEffect(
effect: () => Promise<void | (() => void)>,
deps: DependencyList
) {
useEffect(() => {
let cleanup: (() => void) | void;
effect().then(result => {
cleanup = result;
});
return () => {
if (cleanup) cleanup();
};
}, deps);
}
// 使用
function AsyncComponent() {
useAsyncEffect(async () => {
const data = await fetchData();
console.log(data);
return () => {
console.log('清理');
};
}, []);
}9.2 数据获取的类型
typescript
interface User {
id: number;
name: string;
email: string;
}
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setState(prev => ({ ...prev, loading: true, error: null }));
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const data: T = await response.json();
if (!cancelled) {
setState({ data, loading: false, error: null });
}
} catch (error) {
if (!cancelled) {
setState({
data: null,
loading: false,
error: error as Error
});
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return state;
}
// 使用
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error } = useFetch<User>(
`/api/users/${userId}`
);
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}9.3 事件监听的类型
typescript
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
options?: AddEventListenerOptions
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event: WindowEventMap[K]) => {
savedHandler.current(event);
};
window.addEventListener(eventName, eventListener, options);
return () => {
window.removeEventListener(eventName, eventListener, options);
};
}, [eventName, options]);
}
// 使用
function KeyboardListener() {
useEventListener('keydown', (event) => {
console.log('按键:', event.key);
});
useEventListener('resize', (event) => {
console.log('窗口大小:', window.innerWidth, window.innerHeight);
});
return <div>监听键盘和窗口事件</div>;
}第十部分:最佳实践总结
10.1 Effect使用原则
jsx
/*
Effect使用的黄金法则:
1. 依赖数组规则
- 列出所有使用的响应式值
- 不要省略依赖
- 不要使用对象字面量作为依赖
- 使用ESLint的exhaustive-deps规则
2. 清理函数规则
- 总是清理副作用
- 定时器必须清除
- 事件监听必须移除
- 订阅必须取消
- 异步操作必须取消
3. 性能优化规则
- 拆分独立的Effect
- 使用防抖/节流
- 避免在Effect中创建对象/数组
- 使用useCallback/useMemo稳定依赖
4. 代码组织规则
- 按功能拆分Effect
- 提取为自定义Hook
- 使用清晰的注释说明用途
- 保持Effect简单明了
*/
// 完整示例:遵循所有最佳实践
function BestPracticeExample({ userId, config }) {
const [user, setUser] = useState(null);
const [subscription, setSubscription] = useState(null);
// 1. 拆分独立的Effect
// Effect 1: 获取用户数据
useEffect(() => {
let cancelled = false;
const controller = new AbortController();
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
});
const data = await response.json();
if (!cancelled) {
setUser(data);
}
} catch (error) {
if (error.name !== 'AbortError' && !cancelled) {
console.error(error);
}
}
};
fetchUser();
return () => {
cancelled = true;
controller.abort();
};
}, [userId]); // 只依赖userId
// Effect 2: 订阅更新
useEffect(() => {
if (!user) return;
const sub = subscribeToUserUpdates(user.id);
setSubscription(sub);
return () => {
sub.unsubscribe();
};
}, [user?.id]); // 只依赖user.id
// Effect 3: 应用配置
useEffect(() => {
applyConfig(config);
}, [config]); // 父组件应该使用useMemo稳定config
return <div>{user?.name}</div>;
}10.2 常见模式
jsx
// 模式1: 数据获取
function useFetchPattern(url) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
useEffect(() => {
let cancelled = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then(res => res.json())
.then(data => {
if (!cancelled) {
setState({ data, loading: false, error: null });
}
})
.catch(error => {
if (!cancelled) {
setState({ data: null, loading: false, error });
}
});
return () => {
cancelled = true;
};
}, [url]);
return state;
}
// 模式2: 订阅
function useSubscriptionPattern(source) {
const [value, setValue] = useState(null);
useEffect(() => {
const subscription = source.subscribe(setValue);
return () => {
subscription.unsubscribe();
};
}, [source]);
return value;
}
// 模式3: 定时器
function useIntervalPattern(callback, delay) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay === null) return;
const interval = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(interval);
}, [delay]);
}
// 模式4: 事件监听
function useEventListenerPattern(event, handler, element = window) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(event, eventListener);
return () => {
element.removeEventListener(event, eventListener);
};
}, [event, element]);
}练习题
基础练习
- 实现document.title动态更新
- 创建窗口大小监听器
- 实现定时器组件(开始/暂停/重置)
- 实现键盘快捷键监听
进阶练习
- 完整的数据获取(loading/error/success状态)
- 实现防抖搜索功能
- 解决竞态条件问题
- 实现WebSocket聊天组件
- 创建无限滚动列表
高级练习
- 实现复杂的副作用管理
- 优化Effect性能
- 使用React 19新特性
- 实现Effect调试工具
- 创建类型安全的自定义Effect Hooks
通过本章学习,你已经全面掌握了useEffect的使用,从基础概念到高级模式,从常见错误到性能优化,从调试技巧到TypeScript支持。useEffect是React中最重要的Hook之一,掌握它将使你能够构建健壮、高效的React应用。