React 小误区:派生值 vs useEffect

前言

最近看到小同事的代码,发现经常会把派生值放进 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》三个核心章节。

相关推荐
IT_陈寒2 小时前
折腾一天才明白:Vite的热更新为什么偶尔会罢工
前端·人工智能·后端
AI茶水间管理员2 小时前
学习ClaudeCode源码之Agent核心循环
前端·人工智能·后端
挖稀泥的工人3 小时前
AI聊天界面的布局细节和打字跟随方法
前端·javascript·面试
竹林8183 小时前
从“连接失败”到丝滑登录:我用 ethers.js 连接 MetaMask 的完整踩坑记录
前端·javascript
颜酱3 小时前
图片大模型实践:可灵(Kling)文生图前后端实现
前端·javascript·人工智能
木斯佳3 小时前
前端八股文面经大全:腾讯CSIG实习面(2026-04-10)·面经深度解析
前端·ai·xss·埋点·实习面经
夏暖冬凉3 小时前
npm发布流程(记录遇到的问题)
前端·npm·node.js
XPoet3 小时前
AI 编程工程化:Subagent——给你的 AI 员工打造协作助手
前端·后端·ai编程
心连欣4 小时前
解锁对象遍历:当字符串遇上for...in循环
前端·javascript
Sestid4 小时前
前端Cursor使用指南(后续会更新Claude)
前端·claude·cursor