第 149 章:Hooks 之 useEffect
学习目标
- 理解副作用(Side Effect)概念
- 掌握 useEffect 的三种触发时机
- 学会清理函数
- 避免 useEffect 常见反模式
一、什么是副作用
渲染之外的操作就是副作用:
二、useEffect 基础
tsx
'use client';
import { useEffect, useState } from 'react';
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);
// ① 基础形式
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]); // ② 依赖数组
return <div>{user?.name}</div>;
}三、三种触发时机
3.1 每次渲染都执行(不推荐)
tsx
useEffect(() => {
console.log('每次都执行');
}); // 没有依赖数组3.2 只在挂载时执行一次
tsx
useEffect(() => {
console.log('挂载时执行一次');
fetchInitialData();
}, []); // 空数组3.3 依赖变化时执行
tsx
useEffect(() => {
console.log('userId 变化了');
fetchUser(userId);
}, [userId]); // 依赖 userId四、清理函数
tsx
useEffect(() => {
const timer = setInterval(() => {
console.log('tick');
}, 1000);
// 清理:组件卸载或下次 effect 前执行
return () => {
clearInterval(timer);
};
}, []);4.1 订阅清理
tsx
useEffect(() => {
const sub = ws.subscribe('msg', handleMsg);
return () => sub.unsubscribe();
}, []);4.2 取消请求
tsx
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setUser)
.catch(e => {
if (e.name !== 'AbortError') console.error(e);
});
return () => controller.abort();
}, [userId]);五、常见场景
5.1 数据获取
tsx
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
.finally(() => setLoading(false));
}, []);
if (loading) return <Spinner />;
return <List users={users} />;
}5.2 订阅外部 store
tsx
function useWindowSize() {
const [size, setSize] = useState({ w: 0, h: 0 });
useEffect(() => {
function update() {
setSize({ w: window.innerWidth, h: window.innerHeight });
}
update();
window.addEventListener('resize', update);
return () => window.removeEventListener('resize', update);
}, []);
return size;
}5.3 同步到 localStorage
tsx
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === 'undefined') return initial;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}六、依赖数组的坑
6.1 必须列出所有依赖
tsx
// ❌ 错:漏掉 userId
useEffect(() => {
fetchUser(userId);
}, []);
// ✅ 对
useEffect(() => {
fetchUser(userId);
}, [userId]);6.2 函数依赖
tsx
// ❌ 错:每次渲染 fetchUser 都是新函数
useEffect(() => {
fetchUser(userId);
}, [fetchUser]);
// ✅ 用 useCallback 稳定
const fetchUser = useCallback((id: number) => {
// ...
}, []);
useEffect(() => {
fetchUser(userId);
}, [userId, fetchUser]);七、不要在 useEffect 里做的事
7.1 不要直接处理 props/state 转换
tsx
// ❌ 反模式
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ 用 useMemo 或直接计算
const fullName = `${firstName} ${lastName}`;7.2 不要处理用户事件
tsx
// ❌ 反模式
useEffect(() => {
if (submitted) sendData();
}, [submitted]);
// ✅ 直接在事件处理
function handleSubmit() {
sendData();
setSubmitted(true);
}八、严格模式双调用
开发模式下,React 会故意双调用 effect,用于发现清理问题。
tsx
useEffect(() => {
const sub = api.subscribe();
return () => sub.unsubscribe(); // 必须有清理
}, []);九、本章小结
| 概念 | 关键 |
|---|---|
| 副作用 | 渲染之外的操作 |
| useEffect | useEffect(fn, deps) |
| 依赖 | 列出所有用到的外部值 |
| 清理 | 返回 () => { /* cleanup */ } |
| 一次执行 | 空依赖 [] |
| 严格模式 | 开发模式 effect 会双调用 |
动手练习
- 用 useEffect 获取用户列表并展示
- 写一个 useWindowSize hook
- 写一个 useLocalStorage hook
- 实现一个倒计时组件(setInterval + 清理)
- 实现 useTitle hook(修改 document.title)