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

相关推荐
xiaofeichaichai4 小时前
Webpack
前端·webpack·node.js
问心无愧05134 小时前
ctf show web入门111
android·前端·笔记
唐某人丶4 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界4 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌5 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel6 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3116 小时前
https连接传输流程
前端·面试
徐小夕6 小时前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
threelab6 小时前
Three.js 物理模拟着色器 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器