【React】useMemo 和 useEffect 的用法
两者的核心区别在于“何时执行、做什么、返回什么
作用
useMemo:在渲染期间计算并缓存一个“值”。目的是避免重复的昂贵计算或保持引用稳定(对象/数组/派生结果)。
useEffect:在渲染提交到 DOM 之后执行“副作用”。目的是与外部世界交互或处理需要在渲染后进行的事情(订阅、请求、操作 DOM、日志等)。
执行时机
useMemo:在组件渲染过程中同步运行计算(不触发浏览器绘制前就算完),返回值直接用于本次渲染。
useEffect:在浏览器完成绘制后异步运行(非阻塞渲染)。不会影响本次渲染的输出,只在提交后执行。
返回值
useMemo:返回计算结果(任何值:对象、数组、数字、函数等)。
useEffect:不返回值给渲染;可以返回一个清理函数用于卸载或依赖变化时清理副作用。
依赖变化时的行为
useMemo:依赖不变则复用上次结果;依赖变则在下一次渲染时重新计算值。
useEffect:依赖不变则不重新执行;依赖变则在提交后执行清理函数(若有)然后再运行副作用。
适用场景
useMemo:
重计算优化(过滤、排序、派生数据)。
保持引用稳定,避免把“每次都新建的对象/数组”传给子组件导致不必要渲染。
useEffect:
发起网络请求、订阅/取消订阅、事件监听、操作 DOM、计时器、日志、与外部存储交互等“副作用”。
常见误用对比
不要用 useEffect 去“计算一个值再 setState”来参与同一轮渲染,这会导致额外渲染;应使用 useMemo 在渲染期间直接算出值。
不要用 useMemo 做副作用(如请求、打印日志、修改外部变量);useMemo 仅用于纯计算,不能产生副作用。
与性能相关
useMemo 自身有开销,只有当计算成本高或引用稳定性能明显减少渲染时再用。
useEffect 异步执行,不阻塞渲染,但如果依赖频繁变化且在 Effect 中做重活,也会造成抖动或资源浪费。
简易判断
我只是需要一个由 props/state 派生出来的值,并且它应该在渲染时就可用且无副作用 → useMemo
我需要在组件渲染提交后与外部系统交互或安排清理 → useEffect
小例子:
useMemo:const sorted = useMemo(() => sort(items), [items])
useEffect: useEffect(() => { const id = setInterval(tick, 1000) return () => clearInterval(id) }, [])
实例直观理解
下面用两个并排对比的小例子,直观理解什么时候用 useMemo,什么时候用 useEffect + setState,以及为什么前者更合适做“纯派生计算”。
例子背景
有一份列表 allUsers,你可以通过 selectedRoles 来筛选用户。
我们在界面上展示筛选后的用户。
一、用 useEffect + setState(不太合适的做法)
说明:用 Effect 做纯计算,会导致多一次渲染;同时需要维护额外状态,容易产生同步问题。
代码片段:
function Users({ allUsers, selectedRoles }) {
const [filteredUsers, setFilteredUsers] = useState(allUsers);useEffect(() => {
// 纯计算:根据依赖派生值
const next = selectedRoles.length
? allUsers.filter(u => selectedRoles.includes(u.role))
: allUsers;
setFilteredUsers(next);
}, [allUsers, selectedRoles]);return ({filteredUsers.map(u =>
{u.name} - {u.role}
)}
); }
渲染流程(关键差异)
第1步:组件渲染,filteredUsers 还是上一次的值或初始值。
第2步:提交到 DOM 后,useEffect 运行,计算 next,然后 setFilteredUsers。
第3步:因为 setState,又触发一次重新渲染,UI 才显示最新的筛选结果。
问题
多了一次渲染。
如果有多个类似派生状态,维护依赖、初始值和更新顺序更复杂。
这段逻辑并无副作用,纯粹是算值,不该放在 Effect。
二、用 useMemo(更合适的做法)
说明:在渲染期间直接计算派生值,避免额外渲染,也不需要维护额外状态。
代码片段:
function Users({ allUsers, selectedRoles }) {
const filteredUsers = useMemo(() => {
return selectedRoles.length
? allUsers.filter(u => selectedRoles.includes(u.role))
: allUsers;
}, [allUsers, selectedRoles]);return ({filteredUsers.map(u =>
{u.name} - {u.role}
)}
); }
渲染流程
第1步:组件渲染时,同步计算 filteredUsers(依赖不变则复用缓存)。
第2步:一次渲染就得到正确的 UI,不会再触发额外渲染。
结论
对于“由 props/state 派生出的值”且没有副作用,优先用 useMemo。
你的 filtered = selectedBranches… 这种就是典型的派生计算,应改为 useMemo。
三、什么时候必须用 useEffect(给你一个需要副作用的对比例子)
情景:筛选结果不仅要显示,还要同步到地址栏(URL 查询参数),或写入 localStorage。这就属于副作用,必须用 useEffect。
代码片段:
function Users({ allUsers, selectedRoles }) {
// 派生值还是用 useMemo
const filteredUsers = useMemo(() => {
return selectedRoles.length
? allUsers.filter(u => selectedRoles.includes(u.role))
: allUsers;
}, [allUsers, selectedRoles]);// 用副作用同步到外部世界(URL)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
params.set('roles', selectedRoles.join(','));
const nextUrl = ${window.location.pathname}?${params.toString()};
window.history.replaceState(null, '', nextUrl);
}, [selectedRoles]);return (
{filteredUsers.map(u =>
{u.name} - {u.role}
)}
); }
要点
派生值: 用 useMemo(不产生副作用)。
与外部交互: 用 useEffect(副作用,如 URL、订阅、计时器、请求、DOM 操作)。
如果你用 useEffect 去算 filtered 再 setState,只是把纯计算绕了一圈,增加一次渲染;除非你确实需要把这个值作为“可独立修改的状态”暴露出去或做副作用,否则没必要。
四、再给一个误用示例,对比正确写法
误用:在 useEffect 里计算结果后 setState,然后这个状态只用于当前渲染。
正确:直接 useMemo 返回值,或直接在渲染里算(如果不需要缓存)。
误用:
useEffect(() => {
setFullName(${firstName} ${lastName});
}, [firstName, lastName]);
正确:
const fullName = useMemo(() => ${firstName} ${lastName}, [firstName, lastName]);
// 或者计算很轻:const fullName = ${firstName} ${lastName};
总结规则
我只是需要一个从现有数据派生出来的值、用于当前渲染 → useMemo(或直接算)。
我需要在渲染后做事(与外部交互、事件、计时、请求),或管理清理 → useEffect。
用 useEffect 做纯计算通常是不必要的,会多一次渲染。