React useEffect 深度解析:副作用、争议与现代替代方案
什么是副作用(Side Effects)?
在 React 中,副作用指的是那些不直接参与渲染输出的操作。理解副作用是掌握 useEffect 的第一步。
纯函数 vs 副作用
// ✅ 纯函数:相同输入 → 相同输出,无副作用
function calculateTotal(price: number, quantity: number) {
return price * quantity;
}
// ❌ 有副作用:修改外部状态、发起网络请求
function saveToDatabase(data: any) {
fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
localStorage.setItem('cache', JSON.stringify(data));
console.log('Saved:', data);
}React 中的常见副作用
渲染中...
useEffect 的基本用法
语法结构
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数(可选)
};
}, [依赖项数组]);三种依赖模式
// ⚠️ 每次渲染都执行(很少使用)
useEffect(() => {
console.log('每次渲染都执行');
});// ✅ 只在挂载时执行一次
useEffect(() => {
console.log('组件挂载');
return () => {
console.log('组件卸载');
};
}, []);// ✅ 依赖项变化时执行
useEffect(() => {
console.log('userId 变化:', userId);
fetchUserData(userId);
}, [userId]);useEffect 的争议点
1. 执行时机难以理解
function Counter() {
const [count, setCount] = useState(0);
// ❌ 新手常见误区:以为会立即执行
useEffect(() => {
console.log('当前 count:', count);
}, [count]);
console.log('渲染时 count:', count);
return <button onClick={() => setCount(count + 1)}>点击</button>;
}执行顺序:
渲染中...
2. 依赖项地狱
// ❌ 依赖项越来越多,难以维护
useEffect(() => {
if (userId && isLoggedIn && !isLoading && hasPermission) {
fetchData(userId, filters, sortBy, page, pageSize);
}
}, [userId, isLoggedIn, isLoading, hasPermission, filters, sortBy, page, pageSize]);3. 竞态条件(Race Condition)
// ❌ 快速切换用户时,可能显示错误的数据
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data); // 可能是旧请求的数据!
});
}, [userId]);
return <div>{user?.name}</div>;
}问题演示:
渲染中...
正确做法:
// ✅ 使用清理函数处理竞态
useEffect(() => {
let cancelled = false;
fetchUser(userId).then(data => {
if (!cancelled) {
setUser(data);
}
});
return () => {
cancelled = true;
};
}, [userId]);现代替代方案
1. React 19 的 use Hook
import { use } from 'react';
// ✅ 更简洁的数据获取
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <div>{user.name}</div>;
}
// 使用方式
function App() {
const userPromise = fetchUser('123');
return (
<Suspense fallback={<Loading />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}优势对比:
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetchUser(userId)
.then(data => {
if (!cancelled) {
setUser(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <Loading />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}function UserProfile({ userId }: { userId: string }) {
const user = use(fetchUser(userId));
return <div>{user.name}</div>;
}
// 在父组件处理加载和错误
function App() {
return (
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<UserProfile userId="123" />
</Suspense>
</ErrorBoundary>
);
}2. TanStack Query(React Query)
import { useQuery } from '@tanstack/react-query';
// ✅ 自动处理缓存、重试、竞态
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5分钟内使用缓存
});
if (isLoading) return <Loading />;
if (error) return <Error error={error} />;
return <div>{user.name}</div>;
}TanStack Query 的优势:
渲染中...
3. 事件处理器(而非 useEffect)
// ❌ 不要用 useEffect 处理用户交互
function SearchBox() {
const [query, setQuery] = useState('');
useEffect(() => {
if (query) {
logSearch(query);
}
}, [query]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// ✅ 直接在事件处理器中处理
function SearchBox() {
const [query, setQuery] = useState('');
const handleSearch = (value: string) => {
setQuery(value);
if (value) {
logSearch(value);
}
};
return <input value={query} onChange={e => handleSearch(e.target.value)} />;
}4. 派生状态(而非 useEffect 同步)
// ❌ 不要用 useEffect 同步状态
function TodoList({ todos }: { todos: Todo[] }) {
const [completedCount, setCompletedCount] = useState(0);
useEffect(() => {
setCompletedCount(todos.filter(t => t.completed).length);
}, [todos]);
return <div>已完成: {completedCount}</div>;
}
// ✅ 直接计算派生状态
function TodoList({ todos }: { todos: Todo[] }) {
const completedCount = todos.filter(t => t.completed).length;
return <div>已完成: {completedCount}</div>;
}
// ✅ 如果计算昂贵,使用 useMemo
function TodoList({ todos }: { todos: Todo[] }) {
const completedCount = useMemo(
() => todos.filter(t => t.completed).length,
[todos]
);
return <div>已完成: {completedCount}</div>;
}useEffect 的合理使用场景
虽然有很多替代方案,但 useEffect 在某些场景下仍然是最佳选择:
1. 与外部系统同步
// ✅ WebSocket 连接
function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const ws = new WebSocket(`wss://chat.example.com/${roomId}`);
ws.onmessage = (event) => {
setMessages(prev => [...prev, JSON.parse(event.data)]);
};
return () => {
ws.close();
};
}, [roomId]);
return <MessageList messages={messages} />;
}2. 浏览器 API 集成
// ✅ 监听窗口大小变化
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}3. 第三方库初始化
// ✅ 初始化图表库
function Chart({ data }: { data: number[] }) {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!chartRef.current) return;
const chart = new ChartLibrary(chartRef.current, {
data,
type: 'line',
});
return () => {
chart.destroy();
};
}, [data]);
return <div ref={chartRef} />;
}4. 分析和日志
// ✅ 页面浏览统计
function ProductPage({ productId }: { productId: string }) {
useEffect(() => {
analytics.track('page_view', {
page: 'product',
productId,
timestamp: Date.now(),
});
}, [productId]);
return <ProductDetails productId={productId} />;
}决策流程图
渲染中...
最佳实践总结
✅ 推荐做法
- 优先考虑替代方案:事件处理器 > 派生状态 > TanStack Query > use Hook > useEffect
- 保持 useEffect 简单:一个 useEffect 只做一件事
- 正确处理清理:订阅、定时器、连接都要清理
- 避免依赖项遗漏:使用 ESLint 规则
react-hooks/exhaustive-deps
❌ 避免的做法
- 不要用 useEffect 同步状态:使用派生状态
- 不要用 useEffect 处理用户交互:使用事件处理器
- 不要忽略竞态条件:使用清理函数或 TanStack Query
- 不要过度使用:很多场景不需要 useEffect
总结
useEffect 是 React 中强大但容易误用的工具。随着 React 19 和现代状态管理库的发展,很多传统的 useEffect 使用场景都有了更好的替代方案:
- 数据获取 → TanStack Query 或
useHook - 状态同步 → 派生状态或
useMemo - 用户交互 → 事件处理器
- 外部系统 → 仍然使用 useEffect(合理场景)
理解副作用的本质,选择合适的工具,才能写出更清晰、更可维护的 React 代码。