useEffect
在函数组件中执行副作用操作
什么是副作用?
在 React 中,副作用 (Side Effect) 是指组件渲染之外的操作。副作用包括:
- 数据获取 (API 请求)
- 订阅外部数据源
- 手动修改 DOM
- 定时器 (setTimeout、setInterval)
- 日志记录
在计算机科学中,纯函数是指给定相同输入总是返回相同输出, 并且不产生任何可观察的副作用的函数。 React 组件的渲染应该是纯的,而“副作用”是渲染之外的操作。
基本用法
导入 useEffect
import { useState, useEffect } from 'react';
简单的副作用
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
// 更新文档标题
document.title = `点击了 ${count} 次`;
});
return (
<button onClick={() => setCount(count + 1)}>
点击了 {count} 次
</button>
);
}
工作原理
useEffect 会在每次渲染后执行副作用函数。
React 会在 DOM 更新后调用副作用函数。
如果你有 React 类组件的经验,useEffect
相当于 componentDidMount、
componentDidUpdate 和 componentWillUnmount
的组合。
依赖数组
跳过不必要的重新执行
默认情况下,Effect 会在每次渲染后执行。使用依赖数组可以控制何时执行 Effect:
const [count, setCount] = useState(0);
// 每次渲染后都执行
useEffect(() => {
console.log('每次渲染都执行');
});
// 只在组件挂载时执行一次
useEffect(() => {
console.log('只执行一次');
}, []);
// 只在 count 改变时执行
useEffect(() => {
console.log('count 改变了:', count);
}, [count]);
多个依赖
const [name, setName] = useState('');
const [age, setAge] = useState(0);
useEffect(() => {
console.log('name 或 age 改变了');
}, [name, age]);
永远不要对 Effect 依赖撒谎。 如果 Effect 使用了某个变量,必须将其添加到依赖数组中。 React 会使用 ESLint 规则来帮助你捕获遗漏的依赖。
清理函数
为什么需要清理?
某些副作用需要清理,例如:
- 订阅外部数据源
- 定时器
- 手动添加的事件监听器
返回清理函数
useEffect(() => {
// 副作用
const timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 清理函数
return () => {
clearInterval(timer);
};
}, []);
清理时机
useEffect(() => {
// 1. 组件挂载时执行
console.log('挂载');
return () => {
// 2. 组件卸载前执行
// 或者在下次 effect 执行前执行
console.log('清理');
};
});
实际示例:订阅
function Chat({ userId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// 创建订阅
const subscription = chatAPI.subscribe(userId, (newMessages) => {
setMessages(newMessages);
});
// 清理订阅
return () => {
subscription.unsubscribe();
};
}, [userId]); // 只在 userId 改变时重新订阅
return (
<ul>
{messages.map(msg => (
<li key={msg.id}>{msg.text}</li>
))}
</ul>
);
}
总是返回清理函数,即使 Effect 没有明显的清理需求。 这可以防止内存泄漏和意外行为。
常见模式
1. 数据获取
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 重置状态
setLoading(true);
setError(null);
fetchUser(userId)
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [userId]);
if (loading) return <Spinner />;
if (error) return <Error error={error} />;
return <Profile user={user} />;
}
2. 修改 DOM
function ScrollIndicator() {
const [scrollTop, setScrollTop] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollTop(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return (
<div style={{ width: `${scrollTop}%` }}>
滚动指示器
</div>
);
}
3. 定时器
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(prev => prev - 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return <div>剩余时间: {timeLeft} 秒</div>;
}
4. 条件执行
function Notification({ showMessage }) {
useEffect(() => {
if (showMessage) {
// 只在 showMessage 为 true 时显示通知
showNotification('Hello!');
}
}, [showMessage]);
}
不要在 Effect 中使用条件包裹。 如果需要条件执行,应该在 Effect 函数内部添加条件:
// ❌ 错误
if (condition) {
useEffect(() => {
// ...
});
}
// ✅ 正确
useEffect(() => {
if (condition) {
// ...
}
});
数据获取
基本模式
使用 AbortController 来取消未完成的请求,这是处理竞态条件的最佳实践。
它不仅能防止状态更新,还能真正取消网络请求,节省资源。
使用 AbortController
useEffect(() => {
const controller = new AbortController();
fetchUser(userId, { signal: controller.signal })
.then(data => setData(data))
.catch(error => {
if (error.name !== 'AbortError') {
setError(error);
}
});
return () => {
controller.abort();
};
}, [userId]);
封装自定义 Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(response => response.json())
.then(data => {
setData(data);
setLoading(false);
})
.catch(error => {
if (error.name !== 'AbortError') {
setError(error);
setLoading(false);
}
});
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// 使用
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <Spinner />;
if (error) return <Error error={error} />;
return <UserList users={users} />;
}
React 提供了 use() Hook(React 19+),用于在 Suspense 边界中读取资源(如 Promise 和 Context)。
但 useEffect 仍然是处理副作用的标准方式。
use() 主要用于 Server Components 和 Suspense 场景,不是用来替代 useEffect 的数据获取。
Effect 的生命周期
执行顺序
function Example() {
const [count, setCount] = useState(0);
console.log('1. 组件渲染');
useEffect(() => {
console.log('2. Effect 执行(渲染后)');
return () => {
console.log('3. 清理函数(卸载前或下次 effect 前)');
};
}, [count]);
console.log('4. 组件渲染完成');
}
类组件对比
| 类组件 | 函数组件 + useEffect |
|---|---|
| componentDidMount | useEffect(() => { ... }, []) |
| componentDidUpdate | useEffect(() => { ... }, [deps]) |
| componentWillUnmount | useEffect(() => { return cleanup; }, []) |
不要把 useEffect 看作“在特定生命周期调用函数”。 相反,应该将其视为“将 React 组件与外部系统同步”。
常见错误
1. 缺少依赖
// ❌ 错误:缺少依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, []); // 缺少 count 依赖
// ✅ 正确:添加依赖
useEffect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
}, [count]);
2. 在条件中使用 useEffect
// ❌ 错误:在条件中调用
if (condition) {
useEffect(() => {
// ...
});
}
// ✅ 正确:在 Effect 内部使用条件
useEffect(() => {
if (condition) {
// ...
}
}, [condition]);
3. 在渲染中执行副作用
// ❌ 错误:在渲染中执行副作用
function Component() {
fetchAPI(); // 不要在渲染中执行
return <div />;
}
// ✅ 正确:在 useEffect 中执行
function Component() {
useEffect(() => {
fetchAPI();
}, []);
return <div />;
}
4. 使用 async 函数
// ❌ 错误:不能直接使用 async 函数
useEffect(async () => {
const data = await fetchAPI();
}, []);
// ✅ 正确:在内部定义 async 函数
useEffect(() => {
async function fetchData() {
const data = await fetchAPI();
}
fetchData();
}, []);
// 或使用 IIFE
useEffect(() => {
(async () => {
const data = await fetchAPI();
})();
}, []);
5. 无限循环
// ❌ 错误:无限循环
useEffect(() => {
setCount(count + 1); // 触发重新渲染
}, [count]); // 依赖改变,再次执行 Effect
// 无限循环发生的过程:
// 1. count 改变为 count + 1
// 2. 状态改变触发组件重新渲染
// 3. Effect 检测到依赖 count 发生变化
// 4. 再次执行 Effect,再次调用 setCount(count + 1)
// 5. 回到步骤 1,无限循环...
// ✅ 正确:移除依赖或使用函数更新
useEffect(() => {
setCount(c => c + 1);
}, []); // 只执行一次,因为没有依赖
React 提供了 ESLint 插件 react-hooks/exhaustive-deps,
它会帮助你捕获这些常见错误。
总是遵循 ESLint 的警告!
性能优化
避免不必要的 Effect
// ❌ 不必要的 Effect
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ...
}
// ✅ 直接计算
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = `${firstName} ${lastName}`;
// ...
}
使用 useMemo 缓存计算
import { useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
// 只在 items 或 filter 改变时重新计算
const filteredItems = useMemo(() => {
return items.filter(item => item.type === filter);
}, [items, filter]);
return <List items={filteredItems} />;
}
拆分 Effect
// ❌ 一个 Effect 处理多个不相关的状态
useEffect(() => {
const sub1 = subscribe1();
const sub2 = subscribe2();
return () => {
sub1.unsubscribe();
sub2.unsubscribe();
};
}, []);
// ✅ 拆分为多个 Effect
useEffect(() => {
const sub = subscribe1();
return () => sub.unsubscribe();
}, []);
useEffect(() => {
const sub = subscribe2();
return () => sub.unsubscribe();
}, []);
优先考虑直接计算而不是使用 Effect。 Effect 应该只用于与外部系统的交互。
最佳实践
1. 描述依赖关系
// ✅ 好:所有依赖都明确声明
useEffect(() => {
fetchData(userId, options);
}, [userId, options]);
2. 使用 ESLint 规则
确保 react-hooks/exhaustive-deps 规则已启用。
3. 保持 Effect 简单
// ✅ 好:逻辑清晰的 Effect
useEffect(() => {
const subscription = subscribe();
return () => subscription.unsubscribe();
}, [source]);
4. 提取自定义 Hook
// 提取可复用的逻辑
function useChatSubscription(userId) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const sub = chatAPI.subscribe(userId, setMessages);
return () => sub.unsubscribe();
}, [userId]);
return messages;
}
5. 只同步必要的部分
// ❌ 同步整个对象
useEffect(() => {
setUser(user);
}, [user]);
// ✅ 只同步需要的属性
useEffect(() => {
setUser({ name: user.name });
}, [user.name]);
常见问题
Effect 没有执行?
检查依赖数组是否为空数组 []。
空数组表示只在挂载时执行一次。
Effect 不断执行?
可能是依赖数组中包含了对象或数组,
它们在每次渲染时都是新的引用。
使用 useMemo 或 useCallback 来稳定引用。
清理函数没有执行?
清理函数只在组件卸载或依赖变化时执行。 确保你正确返回了清理函数。
如何在 Effect 中使用 async?
useEffect(() => {
async function fetchData() {
const result = await fetchAPI();
setData(result);
}
fetchData();
}, []);
相关 Hooks
useLayoutEffect - 同步执行的 Effect
useInsertionEffect - CSS-in-JS 库专用
useDeferredValue - 延迟更新
useTransition - 非紧急更新