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》三个核心章节。

相关推荐
cxxcode3 小时前
搞懂 JS 异步的底层真相:从 V8 源码看微任务与宏任务
前端
马可菠萝3 小时前
从零开始,用 Tauri + Vue 3 打造轻量级桌面应用
前端
陆枫Larry3 小时前
JavaScript 字符串处理实战:从 `startsWith` 到链式 `replace` 的避坑指南
前端
天蓝色的鱼鱼3 小时前
你的项目真的需要SSR吗?还是只是你的简历需要?
前端·架构
恋猫de小郭4 小时前
移动端开发稳了?AI 目前还无法取代客户端开发,小红书的论文告诉你数据
前端·flutter·ai编程
文心快码BaiduComate4 小时前
百度云与光本位签署战略合作:用AI Agent 重构芯片研发流程
前端·人工智能·架构
闲云一鹤4 小时前
nginx 快速入门教程 - 写给前端的你
前端·nginx·前端工程化
QCY5 小时前
「完全理解」1 分钟实现自己的 Coding Agent
前端·agent·claude
一拳不是超人5 小时前
Electron主窗口弹框被WebContentView遮挡?独立WebContentView弹框方案详解!
前端·javascript·electron