前言
最近看到小同事的代码,发现经常会把派生值放进 useEffect,这其实是基础不牢固时特别容易踩的坑,那我们就来彻底讲一讲。
一、核心原则
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 派生值(从已有数据计算) | 在渲染时直接计算 | 放进 useEffect 里算再 setState |
| 副作用(请求、订阅、修改外部状态等) | 在 useEffect 里执行 | 直接在渲染时执行 |
二、什么是「派生值」?
派生值 = 根据 props、state、context 等已有数据算出的新数据,官网明确说这属于「渲染逻辑」,纯计算、无任何外部影响:
tsx
// ✅ 纯计算,属于派生值(无任何副作用)
// 1. 根据 firstName 和 lastName 计算全名
const fullName = `${firstName} ${lastName}`;
// 2. 根据 todos 和 tab 过滤列表
const visibleTodos = todos.filter(todo => todo.tab === currentTab);
// 3. 简单数值计算
const sum = a + b;
// 4. 判断逻辑
const isEmpty = !todos.length;
特点:不请求接口、不改外部状态、不操作 DOM,只依赖当前组件内的 props/state,属于纯计算逻辑。
三、什么是「副作用」?
副作用 = 对组件外部产生影响的操作,官网称之为「与外部系统同步」的操作:
tsx
// ✅ 这些都是副作用(对组件外部产生影响)
// 1. 修改浏览器 DOM(修改页面标题)
document.title = `You clicked ${count} times`;
// 2. 订阅外部系统(聊天服务器连接)
const connection = createConnection(serverUrl, roomId);
connection.connect();
// 3. 网络请求(与外部接口同步数据)
fetch('/api/user', { method: 'GET' });
// 4. 操作本地存储(外部存储系统)
localStorage.setItem('preferredTab', currentTab);
// 5. 定时器/计时器(外部计时系统)
const timer = setInterval(() => {
console.log('定时器运行中');
}, 1000);
四、典型错误:派生值放进 useEffect
这是官网《You Might Not Need an Effect》章节专门批评的错误做法,和咱们小同事常犯的错完全一致:
❌ 官网明确批评的错误写法
tsx
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ❌ 错误:派生值(fullName)不该用 useEffect + setState
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
问题:多一次渲染(先渲染,再执行 effect 改 state,再渲染)、可能出现闪烁、逻辑更复杂,完全没必要绕弯子。
✅ 正确写法
tsx
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ 渲染时直接计算,不用额外存 state
const fullName = `${firstName} ${lastName}`;
五、判断清单
不知道该用"直接算"还是"useEffect"?问自己这3个问题就行:
| 问题 | 是 | 否 |
|---|---|---|
| 结果只依赖当前 props/state? | → 渲染时计算(派生值) | → 考虑 useEffect(大概率是副作用) |
| 需要请求、订阅、修改 store? | → 放 useEffect(副作用) | - |
| 需要操作 DOM、改本地存储? | → 放 useEffect(副作用) | - |
六、正确使用 useEffect 的例子
只有处理副作用时,才该用 useEffect,以下全是 React 官网原生示例,覆盖最常用场景:
tsx
// 1. 值变化时修改 DOM(修改页面标题)
const [count, setCount] = useState(0);
useEffect(() => {
// 副作用:修改浏览器 DOM(外部系统)
document.title = `You clicked ${count} times`;
}, [count]);
// 2. 订阅外部系统 + 清理(避免内存泄漏)
import { createConnection } from './chat.js';
function ChatRoom({ roomId, serverUrl }) {
useEffect(() => {
// 副作用:订阅聊天服务器(外部系统)
const connection = createConnection(serverUrl, roomId);
connection.connect();
// 清理函数:组件卸载/依赖变化时取消订阅
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
// 3. 定时器(外部系统)+ 清理
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中');
}, 1000);
// 清理函数:组件卸载时清除定时器
return () => clearInterval(timer);
}, []);
// 4. 官网衍生示例:网络请求(与外部接口同步)
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
// 副作用:发网络请求(外部系统)
fetch(`/api/user/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
七、计算量大时用 useMemo
官网明确说明:useMemo 只用于"计算量极大的派生值",普通派生值没必要用,避免过度优化:
tsx
import { useMemo } from 'react';
function TodoList({ todos, currentTab }) {
// ✅ 计算量大的派生值(过滤超长列表),用 useMemo 缓存
const visibleTodos = useMemo(
() => todos.filter(todo => todo.tab === currentTab),
[todos, currentTab] // 只有依赖变了,才重新计算
);
// ... 渲染 visibleTodos
}
八、总结
派生值:在 render 里直接算(或用 useMemo)。 副作用:在 useEffect 里执行。 口诀:算数在 render,做事在 effect。
补充:所有示例均来自 React 官网(react.dev/),重点参考《Usin... the Effect Hook》《You Might Not Need an Effect》《useMemo》三个核心章节。