一、useEffect 基础概念
1.1 什么是 useEffect
useEffect 是 React Hooks 中用于处理副作用的 Hook。副作用包括:数据获取、订阅、手动修改 DOM、设置定时器等。
1.2 基本语法
javascript
useEffect(() => {
// 副作用代码
return () => {
// 清理函数(可选)
};
}, [dependencies]); // 依赖数组
二、useEffect 的 5 种使用模式
2.1 每次渲染都执行(不推荐)
javascript
useEffect(() => {
console.log('每次组件渲染都会执行');
});
// ⚠️ 注意:没有第二个参数
2.2 仅挂载时执行一次
javascript
useEffect(() => {
console.log('只在组件挂载时执行一次');
// 模拟组件挂载时的操作
const timer = setTimeout(() => {
console.log('定时器执行');
}, 1000);
// 清理函数
return () => {
console.log('组件卸载时清理');
clearTimeout(timer);
};
}, []); // ⭐ 空依赖数组
2.3 依赖特定状态执行
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
console.log(`userId 变化为: ${userId}`);
// 根据 userId 获取用户数据
fetchUserData(userId).then(data => {
setUser(data);
});
}, [userId]); // ⭐ 依赖 userId,当 userId 变化时执行
return <div>{user?.name}</div>;
}
2.4 多个依赖项
javascript
function ChatRoom({ roomId, userId }) {
useEffect(() => {
console.log(`连接到聊天室 ${roomId},用户 ${userId}`);
const connection = createConnection(roomId, userId);
connection.connect();
return () => {
console.log('断开连接');
connection.disconnect();
};
}, [roomId, userId]); // ⭐ 多个依赖项
}
2.5 清理函数(Cleanup)
javascript
useEffect(() => {
// 1. 订阅事件
const handleResize = () => {
console.log('窗口大小变化');
};
window.addEventListener('resize', handleResize);
// 2. 设置定时器
const intervalId = setInterval(() => {
console.log('定时执行');
}, 1000);
// 3. 清理函数(组件卸载或依赖变化时执行)
return () => {
console.log('执行清理');
window.removeEventListener('resize', handleResize);
clearInterval(intervalId);
};
}, []);
三、实际应用场景
3.1 数据获取
javascript
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true; // 防止组件卸载后设置状态
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
if (isMounted) {
setData(result);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchData();
return () => {
isMounted = false;
};
}, []); // 空依赖数组:只获取一次
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
3.2 监听浏览器事件
javascript
function WindowTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
// 添加事件监听
window.addEventListener('resize', handleResize);
// 立即执行一次获取初始值
handleResize();
// 清理函数
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 空数组:只在挂载/卸载时执行
return (
<div>
窗口尺寸: {windowSize.width} x {windowSize.height}
</div>
);
}
3.3 表单输入防抖(Debouncing)
javascript
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
// 设置防抖定时器
const timer = setTimeout(() => {
console.log(`搜索: ${query}`);
// 模拟 API 调用
fetchSearchResults(query).then(data => {
setResults(data);
});
}, 500); // 500ms 延迟
// 清理函数:在 query 变化时清除上一个定时器
return () => {
clearTimeout(timer);
};
}, [query]); // query 变化时触发
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<ul>
{results.map(result => (
<li key={result.id}>{result.name}</li>
))}
</ul>
</div>
);
}
3.4 与 localStorage 交互
javascript
function ThemeToggle() {
const [theme, setTheme] = useState(() => {
// 从 localStorage 读取初始值
const saved = localStorage.getItem('theme');
return saved || 'light';
});
// 1. 保存 theme 到 localStorage
useEffect(() => {
localStorage.setItem('theme', theme);
console.log(`主题已保存: ${theme}`);
}, [theme]); // theme 变化时保存
// 2. 应用主题到 body
useEffect(() => {
document.body.className = theme;
console.log(`主题已应用: ${theme}`);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
切换到 {theme === 'light' ? '深色' : '浅色'} 主题
</button>
);
}
3.5 控制台日志(开发调试)
javascript
function DebugComponent({ value, user }) {
// 记录 value 的变化
useEffect(() => {
console.log('value 变化:', value);
}, [value]);
// 记录 user 对象的变化(深度比较问题)
useEffect(() => {
console.log('user 对象变化:', user);
}, [user]); // ⚠️ 注意:如果 user 是对象,每次都会触发
// 更好的做法:依赖特定属性
useEffect(() => {
console.log('user.id 变化:', user.id);
}, [user.id]); // 只依赖 id
return <div>调试组件</div>;
}
四、高级用法和最佳实践
4.1 多个 useEffect 的使用
javascript
function ComplexComponent({ id, type }) {
// 分开不同的副作用,提高可读性
useEffect(() => {
// 1. 数据获取
fetchData(id);
}, [id]);
useEffect(() => {
// 2. 事件监听
setupEventListeners(type);
return () => {
cleanupEventListeners();
};
}, [type]);
useEffect(() => {
// 3. 文档标题
document.title = `组件 ${id}`;
}, [id]);
return <div>复杂组件</div>;
}
4.2 避免无限循环
javascript
function InfiniteLoopExample() {
const [count, setCount] = useState(0);
// ❌ 错误:无限循环!
// useEffect(() => {
// setCount(count + 1);
// }, [count]); // count 变化会触发 effect,effect 又会改变 count
// ✅ 正确:使用函数式更新
useEffect(() => {
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1); // 函数式更新
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖,只在挂载时设置定时器
return <div>计数: {count}</div>;
}
4.3 自定义 Hook 封装
javascript
// 自定义 Hook:useLocalStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// 使用自定义 Hook
function UserSettings() {
const [settings, setSettings] = useLocalStorage('user-settings', {
theme: 'light',
language: 'zh-CN',
});
const updateTheme = (theme) => {
setSettings(prev => ({ ...prev, theme }));
};
return (
<div>
当前主题: {settings.theme}
<button onClick={() => updateTheme('dark')}>切换主题</button>
</div>
);
}
4.4 useEffect 中的异步函数
javascript
function AsyncEffectExample() {
const [data, setData] = useState(null);
useEffect(() => {
// 方法1:定义异步函数并立即调用
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('获取数据失败:', error);
}
};
fetchData();
// 方法2:使用 IIFE(立即调用函数表达式)
/*
(async () => {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
})();
*/
// ⚠️ 注意:不能直接使用 async function 作为 effect 函数
// useEffect(async () => { ... }, []); // ❌ 错误!
}, []);
return <div>{data ? JSON.stringify(data) : '加载中...'}</div>;
}
五、常见问题和解决方案
5.1 依赖项管理
javascript
function DependencyExample() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(1);
// ✅ 正确:包含所有依赖
useEffect(() => {
console.log(`计算结果: ${count * multiplier}`);
}, [count, multiplier]); // 包含所有使用的值
// ❌ 错误:遗漏依赖(ESLint 会警告)
// useEffect(() => {
// console.log(`计算结果: ${count * multiplier}`);
// }, [count]); // 缺少 multiplier
// 特殊情况:如果确实不需要依赖变化时触发
const stableCallback = useCallback(() => {
console.log(`稳定回调,count: ${count}`);
}, [count]); // useCallback 缓存函数
useEffect(() => {
// 使用稳定回调
stableCallback();
}, [stableCallback]); // 依赖稳定的回调
return (
<div>
<button onClick={() => setCount(c => c + 1)}>增加计数</button>
<button onClick={() => setMultiplier(m => m * 2)}>双倍乘数</button>
</div>
);
}
5.2 条件执行
javascript
function ConditionalEffect({ shouldFetch, id }) {
const [data, setData] = useState(null);
useEffect(() => {
// 条件执行
if (!shouldFetch || !id) {
return; // 提前返回,不执行副作用
}
let isMounted = true;
const fetchData = async () => {
try {
const response = await fetch(`/api/data/${id}`);
const result = await response.json();
if (isMounted) {
setData(result);
}
} catch (error) {
console.error(error);
}
};
fetchData();
return () => {
isMounted = false;
};
}, [shouldFetch, id]); // 依赖条件
return <div>{data ? data.name : '未获取数据'}</div>;
}
六、性能优化
6.1 避免不必要的执行
javascript
function OptimizedComponent({ items, filter }) {
// 计算衍生数据(使用 useMemo)
const filteredItems = useMemo(() => {
console.log('重新计算 filteredItems');
return items.filter(item => item.includes(filter));
}, [items, filter]); // 依赖 items 和 filter
// 只有 filteredItems 变化时才执行
useEffect(() => {
console.log('过滤结果变化,执行副作用');
// 处理 filteredItems
}, [filteredItems]); // 依赖计算后的值
return (
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
6.2 使用 useRef 存储不触发重新渲染的值
javascript
function RefInEffect() {
const [count, setCount] = useState(0);
const previousCountRef = useRef();
const intervalRef = useRef();
useEffect(() => {
// 存储上一次的值
previousCountRef.current = count;
});
useEffect(() => {
// 使用 ref 存储定时器 ID
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => {
clearInterval(intervalRef.current);
};
}, []);
const previousCount = previousCountRef.current;
return (
<div>
当前计数: {count}
{previousCount !== undefined && (
<div>上一次计数: {previousCount}</div>
)}
</div>
);
}
七、useEffect 与 useLayoutEffect 的区别
javascript
function EffectComparison() {
const [value, setValue] = useState(0);
// useEffect:在浏览器绘制后异步执行
useEffect(() => {
console.log('useEffect 执行');
if (value === 0) {
// 可能会看到闪烁
setValue(100);
}
}, [value]);
// useLayoutEffect:在浏览器绘制前同步执行
useLayoutEffect(() => {
console.log('useLayoutEffect 执行');
// 适合操作 DOM,避免闪烁
// document.body.style.backgroundColor = 'red';
}, [value]);
return (
<button onClick={() => setValue(0)}>
重置为 0
</button>
);
}
总结要点
-
依赖数组决定执行时机:
[]:只在挂载和卸载时执行[dep1, dep2]:依赖变化时执行- 无数组:每次渲染都执行
-
清理函数:
- 返回一个函数用于清理副作用
- 在组件卸载或依赖变化前执行
-
最佳实践:
- 分离不同的副作用到多个 useEffect
- 包含所有必要的依赖
- 使用自定义 Hook 封装复杂逻辑
- 注意无限循环问题
-
常见用途:
- 数据获取
- 事件监听
- 订阅/取消订阅
- 操作 DOM
- 设置/清除定时器
记住:useEffect 是处理副作用的工具,不是处理同步状态逻辑的地方。对于复杂的状态逻辑,考虑使用 useReducer 或其他状态管理方案。