完整教程:深入理解 React useEffect
2025-09-16 20:53 tlnshuju 阅读(391) 评论(0) 收藏 举报一、useEffect基础概念
1、什么是副作用(Side Effects)?
在React中,副作用是指那些与组件渲染结果无关的操作,例如:
- 数据获取(API调用)
- 手动修改DOM
- 设置订阅或定时器
- 记录日志
2、useEffect的基本语法
import { useEffect
} from 'react';
function MyComponent() {
useEffect(() =>
{
// 副作用逻辑在这里执行
return () =>
{
// 清理函数(可选)
};
}, [dependency1, dependency2]);
// 依赖数组(可选)
}
二、useEffect的三种适用方式
1、每次渲染后都执行
useEffect(() =>
{
// 每次组件渲染后都会执行
console.log('组件已渲染或更新');
});
2、仅在挂载时执行一次
useEffect(() =>
{
// 只在组件挂载时执行一次
console.log('组件已挂载');
return () =>
{
// 清理函数,在组件卸载时执行
console.log('组件即将卸载');
};
}, []);
// 空依赖数组
3、依赖特定值变化时执行
useEffect(() =>
{
// 当 count 或 name 变化时执行
console.log(`Count: ${count
}, Name: ${name
}`);
return () =>
{
// 清理上一次的 effect
console.log('清理上一次的 effect');
};
}, [count, name]);
// 依赖数组
三、useEffect执行机制详解

四、常见使用场景
1、数据获取
import { useState, useEffect
} from 'react';
function UserProfile({ userId
}) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() =>
{
// 重置状态
setLoading(true);
setError(null);
const fetchUser = async () =>
{
try {
const response = await fetch(`/api/users/${userId
}`);
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
// 不需要清理函数,因为 fetch 会自动取消
}, [userId]);
// 当 userId 变化时重新获取
if (loading) return <div>加载中...<
/div>
;
if (error) return <div>错误: {error
}<
/div>
;
return (
<div>
<h1>
{user.name
}<
/h1>
<p>
{user.email
}<
/p>
<
/div>
);
}
2、事件监听器
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() =>
{
const handleResize = () =>
{
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
// 添加事件监听
window.addEventListener('resize', handleResize);
// 清理函数:移除事件监听
return () =>
{
window.removeEventListener('resize', handleResize);
};
}, []);
// 空数组表示只在挂载/卸载时执行
return (
<div>
窗口尺寸: {windowSize.width
} x {windowSize.height
}
<
/div>
);
}
3、定时器
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() =>
{
const intervalId = setInterval(() =>
{
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// 清理函数:清除定时器
return () =>
{
clearInterval(intervalId);
};
}, []);
// 空依赖数组,只在挂载时设置定时器
return <div>已运行: {seconds
} 秒<
/div>
;
}
4、手动操作DOM
function FocusInput() {
const inputRef = useRef(null);
useEffect(() =>
{
// 组件挂载后自动聚焦输入框
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
// 空数组表示只在挂载时执行
return <input ref={inputRef
} placeholder="自动聚焦" />
;
}
五、依赖数组的详细说明
1、依赖数组的规则
// ✅ 正确:包含所有依赖
useEffect(() =>
{
document.title = `${title
} - ${count
} 次点击`;
}, [title, count]);
// 所有依赖都声明
// ❌ 错误:缺少依赖
useEffect(() =>
{
document.title = `${title
} - ${count
} 次点击`;
}, [title]);
// 缺少 count 依赖
// ✅ 正确:使用函数式更新避免依赖
useEffect(() =>
{
const timer = setInterval(() =>
{
setCount(prevCount => prevCount + 1);
// 不需要 count 依赖
}, 1000);
return () =>
clearInterval(timer);
}, []);
// 空依赖数组
2、处理对象和函数依赖
function UserProfile({ user
}) {
// 使用 useMemo 记忆化对象
const userStatus = useMemo(() =>
({
isActive: user.active,
statusText: user.active ? '活跃' : '非活跃'
}), [user.active]);
// 只有当 user.active 变化时重新计算
// 使用 useCallback 记忆化函数
const updateUser = useCallback((updates) =>
{
// 更新用户逻辑
}, [user.id]);
// 依赖 user.id
useEffect(() =>
{
// 使用记忆化的值和函数
console.log(userStatus);
updateUser({
lastLogin: new Date()
});
}, [userStatus, updateUser]);
// 依赖记忆化的值
return <div>用户状态: {userStatus.statusText
}<
/div>
;
}
六、useEffect的进阶用法
1、多个useEffect的使用
function ComplexComponent({ userId, autoRefresh
}) {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
// 获取用户数据
useEffect(() =>
{
fetchUser(userId).then(setUser);
}, [userId]);
// 获取通知(依赖用户数据)
useEffect(() =>
{
if (user) {
fetchNotifications(user.id).then(setNotifications);
}
}, [user]);
// 依赖 user
// 自动刷新通知
useEffect(() =>
{
if (!autoRefresh || !user) return;
const intervalId = setInterval(() =>
{
fetchNotifications(user.id).then(setNotifications);
}, 30000);
return () =>
clearInterval(intervalId);
}, [autoRefresh, user]);
// 依赖 autoRefresh 和 user
return (
<div>
{
/* 渲染逻辑 */
}
<
/div>
);
}
2、适用自定义Hook封装useEffect
// 自定义 Hook:使用防抖的搜索
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() =>
{
const handler = setTimeout(() =>
{
setDebouncedValue(value);
}, delay);
return () =>
{
clearTimeout(handler);
};
}, [value, delay]);
// 依赖 value 和 delay
return debouncedValue;
}
// 在组件中使用
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const debouncedQuery = useDebounce(query, 500);
useEffect(() =>
{
if (debouncedQuery) {
searchAPI(debouncedQuery).then(setResults);
} else {
setResults([]);
}
}, [debouncedQuery]);
// 依赖防抖后的查询
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>
);
}
七、常见问题与解决方案
1、无限循环问题
// ❌ 错误:导致无限循环
const [count, setCount] = useState(0);
useEffect(() =>
{
setCount(count + 1);
// 每次渲染都会更新 count,触发重新渲染
}, [count]);
// 依赖 count
// ✅ 正确:使用函数式更新或无依赖
useEffect(() =>
{
setCount(prevCount => prevCount + 1);
// 不依赖外部 count 值
}, []);
// 空依赖数组
2、异步操作处理
function AsyncComponent() {
const [data, setData] = useState(null);
useEffect(() =>
{
let isMounted = true;
// 跟踪组件是否挂载
const fetchData = async () =>
{
try {
const result = await fetch('/api/data');
const jsonData = await result.json();
if (isMounted) {
setData(jsonData);
// 只在组件仍挂载时更新状态
}
} catch (error) {
if (isMounted) {
console.error('获取数据失败:', error);
}
}
};
fetchData();
return () =>
{
isMounted = false;
// 组件卸载时设置为 false
};
}, []);
return <div>
{data ? data.message : '加载中...'
}<
/div>
;
}
3、依赖函数的问题
function ProblematicComponent() {
const [count, setCount] = useState(0);
const logCount = () =>
{
console.log('当前计数:', count);
};
// ❌ 问题:logCount 在每次渲染都是新函数
useEffect(() =>
{
logCount();
}, [logCount]);
// 导致每次渲染都执行
// ✅ 解决方案1:将函数移到 useEffect 内部
useEffect(() =>
{
const logCount = () =>
{
console.log('当前计数:', count);
};
logCount();
}, [count]);
// 只依赖 count
// ✅ 解决方案2:使用 useCallback 记忆化函数
const logCountMemoized = useCallback(() =>
{
console.log('当前计数:', count);
}, [count]);
// 依赖 count
useEffect(() =>
{
logCountMemoized();
}, [logCountMemoized]);
// 依赖记忆化的函数
return <button onClick={
() =>
setCount(c => c + 1)
}>增加<
/button>
;
}
八、性能优化技巧
1、条件执行Effect
function ExpensiveComponent({ data, shouldProcess
}) {
useEffect(() =>
{
if (shouldProcess) {
// 只有 shouldProcess 为 true 时才执行昂贵操作
performExpensiveOperation(data);
}
}, [data, shouldProcess]);
// 仍然声明所有依赖
});
2、使用useMemo优化依赖
function OptimizedComponent({ items, filter
}) {
// 使用 useMemo 避免不必要的重新计算
const filteredItems = useMemo(() =>
{
return items.filter(item => item.includes(filter));
}, [items, filter]);
// 只有当 items 或 filter 变化时重新计算
// effect 只依赖记忆化的值
useEffect(() =>
{
console.log('过滤后的项目:', filteredItems);
}, [filteredItems]);
// 依赖记忆化的数组
return (
<ul>
{filteredItems.map(item =>
(
<li key={item
}>
{item
}<
/li>
))
}
<
/ul>
);
}
3、避免不必要的Effect
// ❌ 不必要的 effect:可以在渲染期间直接计算
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() =>
{
setFullName(`${firstName
} ${lastName
}`);
}, [firstName, lastName]);
// ✅ 更好的方案:在渲染期间直接计算
const fullName = `${firstName
} ${lastName
}`;
九、最佳实践总结
1、明确依赖: 始终声明所有effect中使用的依赖项
2、适当清理: 对于订阅、定时器等,一定要提供清理函数
3、分离关注点: 使用多个useEffect分离不同的逻辑
4、避免无限循环: 谨慎设置状态,避免创建渲染循环
5、性能优化: 使用useMemo和useCallback优化依赖项
6、条件执行: 在effect内部添加条件判断,避免不必要的执行
7、异步处理: 正确处理异步操作的清理和竞态条件
总结
useEffect 是React函数组件的核心Hook,它使得副作用管理变得更加声明式和可预测。通过理解其执行机制、正确使用依赖数组、实现适当的清理逻辑,你可以编写出高效、可靠的React组件。
记住,useEffect 的核心思想是将副作用与渲染逻辑分离,让组件更专注于渲染UI,而将副作用操作放在统一的地方管理。这种分离使得代码更容易理解、测试和维护。
浙公网安备 33010602011771号