← 返回场景决策

React 场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 7 大核心场景全解析

useMemo vs useCallback useEffect React.memo 状态管理选型 Suspense & Portal SSR vs CSR vs SSG useTransition

1. useMemo vs useCallback — 记忆化决策

useMemo = 把结果写在备忘录 下次直接翻备忘录,不用重新算
useCallback = 把手机号存通讯录 不用每次打电话都重新背号码
类比理解:useMemo 就像把计算结果写在备忘录里——下次同样的输入,直接翻备忘录看答案,不用重新算。useCallback 就像把常用手机号存到通讯录——下次打电话直接从通讯录调出来,不用每次都重新背一遍号码。两者都是为了"记住"上次的结果/引用,避免不必要的重复工作。
✅ 什么时候用 useMemo

1. 计算开销大:过滤/排序大数组、复杂的数据变换

当数据量很大(上千条),每次渲染都重新 filter/sort 会导致明显卡顿。useMemo 只在依赖变化时才重新计算,跳过无意义的重复计算。

2. 引用稳定性:创建新对象/数组作为 useEffect 的依赖

如果 useEffect 的依赖是每次渲染都 new 出来的对象/数组,会导致无限循环。用 useMemo 包一层保证引用稳定。

3. 创建新对象传给子组件(配合 React.memo)

父组件每次渲染都创建新的 style 对象或 config 对象传给子组件,即使子组件用了 React.memo 也挡不住(引用每次都变)。useMemo 包住就能稳定引用。

✅ 什么时候用 useCallback

1. 传给子组件的回调函数(配合 React.memo)

父组件每次渲染时,内联的回调函数会创建新的引用,导致 React.memo 子组件误判为 props 变了。useCallback 让回调引用稳定,子组件不会无谓 re-render。

2. 回调作为 useEffect 的依赖

如果某个函数被 useEffect 依赖,且函数定义在组件内部,每次渲染都会生成新函数触发 effect。useCallback 稳定引用,effect 只在真正需要时触发。

❌ 什么时候不用 / 边界

1. 简单计算不需要 useMemo

两个数字相加、字符串拼接、简单的布尔判断——这些计算比 useMemo 本身的开销还小。useMemo 自己也有成本(对比依赖项、维护缓存),别为了"看起来高级"而滥用。

2. 基本类型不需要记忆化

string、number、boolean 本身就是值比较,不存在引用变化问题。只有 object、array、function 才需要担心引用稳定性。

3. 过早优化——先用后优化

不要一上来就把所有计算都 useMemo。先用 React DevTools Profiler 找到瓶颈,再针对性优化。"过早优化是万恶之源"——Donald Knuth。

4. 不传给子组件的回调不需要 useCallback

如果回调只在本组件内部使用(不传给子组件、不放进 useEffect 依赖),useCallback 没有任何收益——函数会在组件内直接被调用,不需要稳定引用。

代码示例:useMemo 过滤大列表
📀 react-app
📁 src/hooks/
useMemo-demo.jsx
useCallback-demo.jsx
import { useState, useMemo } from 'react' // 模拟 10000 条数据 const allProducts = Array.from({ length: 10000 }, (_, i) => ({ id: i + 1, name: `商品 ${i + 1}`, price: Math.round(Math.random() * 1000), category: ['电子', '服装', '食品', '家居'][i % 4], })) function ProductList() { const [search, setSearch] = useState('') const [category, setCategory] = useState('all') const [sortBy, setSortBy] = useState('id') // ✅ useMemo:只有 search/category/sortBy 变了才重新过滤排序 // 没有 useMemo 的话,每次输入搜索词都会对 10000 条数据做过滤+排序 const filteredProducts = useMemo(() => { console.log('执行过滤排序...') // 验证:只在依赖变化时打印 let result = allProducts // 按分类过滤 if (category !== 'all') { result = result.filter(p => p.category === category) } // 按搜索词过滤 if (search) { result = result.filter(p => p.name.includes(search)) } // 排序 if (sortBy === 'price') { result = [...result].sort((a, b) => a.price - b.price) } else if (sortBy === 'name') { result = [...result].sort((a, b) => a.name.localeCompare(b.name)) } return result }, [search, category, sortBy]) // 依赖:只有这三个变了才重新算 return ( <div> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="搜索商品..." /> <select value={category} onChange={e => setCategory(e.target.value)}> <option value="all">全部分类</option> <option value="电子">电子</option> <option value="服装">服装</option> <option value="食品">食品</option> <option value="家居">家居</option> </select> <select value={sortBy} onChange={e => setSortBy(e.target.value)}> <option value="id">默认排序</option> <option value="price">价格排序</option> <option value="name">名称排序</option> </select> <p>共 {filteredProducts.length} 件商品</p> <ul> {filteredProducts.slice(0, 50).map(p => ( <li key={p.id}>{p.name} - ¥{p.price} ({p.category})</li> ))} </ul> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
代码示例:useCallback 稳定子组件 props
📀 react-app
📁 src/hooks/
useCallback-demo.jsx
useMemo-demo.jsx
import { useState, useCallback, memo } from 'react' // ✅ 子组件用 React.memo 包裹,只有 props 真正变化才 re-render const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) { console.log(`渲染: ${todo.text}`) return ( <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}> <input type="checkbox" checked={todo.done} onChange={() => onToggle(todo.id)} /> <span style={{ textDecoration: todo.done ? 'line-through' : 'none' }}> {todo.text} </span> <button onClick={() => onDelete(todo.id)}>删除</button> </div> ) }) function TodoApp() { const [todos, setTodos] = useState([ { id: 1, text: '学习 React', done: false }, { id: 2, text: '写代码', done: false }, { id: 3, text: '刷算法题', done: true }, ]) const [input, setInput] = useState('') // ✅ useCallback:保证 onToggle 引用稳定,不会触发子组件无谓 re-render const handleToggle = useCallback((id) => { setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t )) }, []) // 空依赖:setTodos 本身稳定,不需要依赖 // ✅ useCallback:保证 onDelete 引用稳定 const handleDelete = useCallback((id) => { setTodos(prev => prev.filter(t => t.id !== id)) }, []) // ❌ 如果不用 useCallback,每次输入时 input 变化 → 父组件 re-render // → handleToggle/handleDelete 是新函数 → TodoItem 的 props 变了 // → 即使 React.memo 也挡不住 → 所有 TodoItem 都 re-render return ( <div> <input value={input} onChange={e => setInput(e.target.value)} placeholder="新增待办..." /> <button onClick={() => { setTodos(prev => [...prev, { id: Date.now(), text: input, done: false }]) setInput('') }}>添加</button> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} onDelete={handleDelete} /> ))} </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
决策表:useMemo vs useCallback vs 不用
场景 useMemo useCallback 都不用
过滤/排序大数组 ✅ 包住计算结果 ❌ 每次渲染都算
传给子组件的回调 ✅ 稳定函数引用 ❌ 每次渲染新函数
创建新对象作为依赖 ✅ 稳定对象引用 ❌ 无限循环
简单数值计算 a+b ❌ 优化成本 > 收益 ✅ 直接算
只在组件内用的函数 ❌ 多此一举 ✅ 直接定义
基本类型 (string/number) ❌ 不需要 ✅ 值比较无问题
作为 useEffect 依赖的函数 ✅ 防止 effect 反复触发 ❌ 每次触发 effect
经验法则:useMemo 记住的是计算结果(值),useCallback 记住的是函数引用。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。如果你不确定要不要用,先不用——等 React DevTools Profiler 告诉你有性能问题再加。

2. useEffect — 依赖数组陷阱

useEffect = 入住酒店 入住开灯 = 挂载执行
return 清理 = 退房关灯 走的时候要关灯拔卡
忘了清理 = 灯一直亮着 内存泄漏 / 定时器狂跑 / 监听器堆积
类比理解:useEffect 就像住酒店——入住时开灯开空调(挂载执行 effect),退房时必须关灯关空调(return cleanup)。如果你忘了清理,灯就一直亮着浪费电(内存泄漏、定时器一直在跑、事件监听器堆积)。依赖数组就像房卡——卡里的信息变了(依赖变化),房间服务就要重新来一次。
✅ 正确用法

1. 依赖数组必须包含所有用到的值

useEffect 内部用到的 state、props、任何变量,都必须放进依赖数组。React 的 exhaustive-deps lint 规则会帮你检查。漏掉依赖 = 闭包里的值是旧的。

2. 返回清理函数(cleanup)

设置了定时器、订阅、事件监听,必须在 cleanup 中清除。否则组件卸载后这些东西还在跑,造成内存泄漏。

3. 空数组 [] 只执行一次(等同 componentDidMount)

只想在挂载时请求一次数据?用 []。但要确保 effect 内部不依赖任何可能变化的值,否则会读到旧值。

❌ 常见错误

1. 遗漏依赖 → 闭包旧值(stale closure)

在 effect 中用了 count 但没放进依赖数组,effect 里的 count 永远是初始值。这是 React hooks 最常见的 bug 之一。

2. 对象引用每次 new → 无限循环

依赖数组里放了对象 { page: 1 },每次渲染都是新对象(引用不同),effect 不停触发 → setState → 渲染 → 新对象 → effect 触发...死循环。

3. 不清理 → 内存泄漏

setInterval / addEventListener / WebSocket 订阅,不在 cleanup 里 clearInterval / removeEventListener / 关闭连接 = 内存泄漏 + 幽灵回调。

4. 把 effect 当成"渲染后执行"的万能钩子

useEffect 是同步外部系统的桥梁(API、DOM、定时器、订阅)。如果只是根据 state 派生数据,用 useMemo;如果只是响应 state 变化执行逻辑,先想想能不能在事件处理函数中做。

代码示例:轮询请求 + 清理
📀 react-app
📁 src/components/
useEffect-cleanup.jsx
Timer.jsx
import { useState, useEffect } from 'react' function LiveDashboard() { const [data, setData] = useState(null) const [status, setStatus] = useState('loading') useEffect(() => { // ✅ 正确:开始轮询 const pollInterval = setInterval(async () => { try { const res = await fetch('/api/dashboard') const json = await res.json() setData(json) setStatus('ok') } catch { setStatus('error') } }, 5000) // ✅ 关键:清理函数,组件卸载时停止轮询 // 如果不清理,切走后定时器还在跑,setData 作用在已卸载的组件上 return () => { console.log('停止轮询') clearInterval(pollInterval) } }, []) // 空数组:只在挂载时启动轮询 if (status === 'loading') return <div>加载中...</div> if (status === 'error') return <div>加载失败</div> return ( <div> <h2>实时仪表盘(每5秒刷新)</h2> <pre>{JSON.stringify(data, null, 2)}</pre> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
代码示例:事件监听 + 清理 + 依赖
📀 react-app
📁 src/components/
useEffect-deps.jsx
SearchBar.jsx
import { useState, useEffect } from 'react' function MouseTracker() { const [pos, setPos] = useState({ x: 0, y: 0 }) const [tracking, setTracking] = useState(true) useEffect(() => { if (!tracking) return // 不追踪时什么都不注册 const handleMove = (e) => { setPos({ x: e.clientX, y: e.clientY }) } window.addEventListener('mousemove', handleMove) // ✅ 清理:移除监听器(每次 effect 重新执行前也会先清理) return () => { window.removeEventListener('mousemove', handleMove) } }, [tracking]) // ✅ tracking 变了要重新注册/取消 return ( <div> <button onClick={() => setTracking(t => !t)}> {tracking ? '停止追踪' : '开始追踪'} </button> {tracking && <p>鼠标位置: ({pos.x}, {pos.y})</p>} </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
代码示例:对象依赖导致无限循环 & 修复
📀 react-app
📁 src/components/
useEffect-stale.jsx
Timer.jsx
import { useState, useEffect, useMemo } from 'react' function SearchResults() { const [query, setQuery] = useState('') const [page, setPage] = useState(1) const [results, setResults] = useState([]) // ❌ 错误写法:每次渲染 params 都是新的对象 → 无限循环 // const params = { query, page } // useEffect(() => { // fetchResults(params).then(setResults) // }, [params]) // ← params 每次都是新引用! // ✅ 方案 1:把具体字段放进依赖数组 useEffect(() => { fetchResults({ query, page }).then(setResults) }, [query, page]) // ✅ 基本类型,值比较 // ✅ 方案 2:如果确实需要对象,用 useMemo 稳定引用 const params = useMemo(() => ({ query, page }), [query, page]) // useEffect(() => { // fetchResults(params).then(setResults) // }, [params]) // ← 现在 params 引用稳定了 return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <button onClick={() => setPage(p => p + 1)}>下一页</button> <ul>{results.map((r, i) => <li key={i}>{r}</li>)}</ul> </div> ) } // 模拟 API async function fetchResults({ query, page }) { const res = await fetch(`/api/search?q=${query}&page=${page}`) return res.json() }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
决策表:useEffect 依赖配置
依赖配置 执行时机 适用场景 风险
不传依赖数组 每次渲染后 几乎不用 性能灾难
[] 空数组 仅挂载时 初始化数据、启动轮询 可能读到旧值
[a, b] a 或 b 变化时 响应状态变化 最常用
[obj] 对象引用 引用变化时 无限循环风险
核心原则:useEffect 是与外部系统同步的桥梁(API、DOM、timer、subscription)。如果你的 effect 不涉及任何外部系统,先想想能不能用 useMemo(派生数据)或在事件处理器中处理(用户交互逻辑)。不要把 useEffect 当"监听 state 变化"的工具。

3. React.memo — 什么时候加

React.memo = 门禁卡系统 刷卡才能进(props变了才re-render)
办卡也要时间 浅比较 props 有成本,组件太轻反而更慢
类比理解:React.memo 就像写字楼门禁卡系统——刷卡才能进(props 变了才 re-render),不刷卡不进(props 没变就跳过)。但办卡本身也要时间和成本(浅比较 props 需要遍历属性),如果楼里全是频繁进出的人(组件很轻/props 每次都变),装门禁反而添堵。
✅ 什么时候加 React.memo

1. 纯展示组件 + 父组件频繁 re-render

父组件有频繁的状态变化(如输入框、定时器),但某个子组件只是展示,props 不变。加 memo 避免子组件跟着白跑。

2. 列表项组件

渲染 100 个列表项,只改了其中 1 个。不加 memo 的话 100 个全部 re-render;加了 memo,只有变的那 1 个会更新。

3. props 很少变化的组件

比如一个用户头像组件,props 是 userId 和 size,几乎不变。memo 几乎每次都能命中缓存,跳过渲染。

❌ 什么时候不加 / 边界

1. props 每次都是新引用

父组件每次渲染都传新的 object/array/function(没用 useMemo/useCallback),memo 的浅比较每次都不相等 → memo 白加了,还多了比较成本。

2. 组件很轻(渲染快)

如果一个组件渲染耗时 < 1ms,加 memo 的浅比较成本可能比直接渲染还高。优化了个寂寞。

3. children 经常变化

如果组件接收 children 且 children 每次都不同(如包含动态内容),memo 永远拦不住,因为 children 的引用每次都在变。

代码示例:列表项优化
📀 react-app
📁 src/components/
CallbackInline.jsx
SearchBar.jsx
import { useState, useCallback, memo } from 'react' // ✅ React.memo 包裹列表项:只有自己的 props 变了才 re-render const UserCard = memo(function UserCard({ user, onFollow }) { console.log(`渲染 UserCard: ${user.name}`) return ( <div style={{ border: '1px solid #333', padding: 12, margin: 8 }}> <img src={user.avatar} alt={user.name} width="40" /> <span>{user.name}</span> <button onClick={() => onFollow(user.id)}> {user.followed ? '已关注' : '关注'} </button> </div> ) }) function UserList() { const [users, setUsers] = useState([ { id: 1, name: 'Alice', avatar: '/a.png', followed: false }, { id: 2, name: 'Bob', avatar: '/b.png', followed: false }, { id: 3, name: 'Carol', avatar: '/c.png', followed: true }, ]) const [search, setSearch] = useState('') // ✅ useCallback 稳定 onFollow 引用 const handleFollow = useCallback((id) => { setUsers(prev => prev.map(u => u.id === id ? { ...u, followed: !u.followed } : u )) }, []) // ❌ 如果不 memo + useCallback: // 每次输入搜索词 → 父组件 re-render → handleFollow 新引用 // → 所有 UserCard 的 props 变了 → 3 个全部 re-render // ✅ 有了 memo + useCallback: // 搜索时 → 父组件 re-render → handleFollow 引用不变 // → memo 浅比较 props 不变 → UserCard 全部跳过渲染 return ( <div> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="搜索用户..." /> {users .filter(u => u.name.toLowerCase().includes(search.toLowerCase())) .map(u => ( <UserCard key={u.id} user={u} onFollow={handleFollow} /> )) } </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
代码示例:自定义比较函数(areEqual)
📀 react-app
📁 src/components/
MemoDemo.jsx
ExpensiveList.jsx
import { memo } from 'react' // ✅ 第二个参数:自定义比较函数 // 返回 true = props 相同,跳过渲染 // 返回 false = props 不同,需要渲染 // 注意:和 shouldComponentUpdate 返回值相反! const ExpensiveChart = memo( function ExpensiveChart({ dataPoints, width, height }) { console.log('渲染图表...') // 假设这是一个很重的图表渲染 return ( <canvas width={width} height={height}> {/* 渲染图表 */} </canvas> ) }, // 自定义比较:只在 dataPoints 内容或尺寸变化时才渲染 (prevProps, nextProps) => { // 比较基本类型 if (prevProps.width !== nextProps.width) return false if (prevProps.height !== nextProps.height) return false // 比较 dataPoints 数组内容(深层比较) if (prevProps.dataPoints.length !== nextProps.dataPoints.length) return false return prevProps.dataPoints.every( (p, i) => p.x === nextProps.dataPoints[i].x && p.y === nextProps.dataPoints[i].y ) // 返回 true = 相同 = 跳过渲染 } )
面试金句 React.memo 是高阶组件,对函数组件进行浅比较 props 的记忆化。使用场景:子组件渲染成本高且 props 不常变。陷阱:每次 render 创建新函数/对象作为 props 会使 memo 失效(需配合 useCallback/useMemo)。React Compiler(2024+)将自动处理大部分 memo 需求。
决策表:要不要加 React.memo
场景 加 memo? 原因
纯展示 + 父频繁 re-render ✅ 加 有效跳过无意义渲染
列表项(数量多) ✅ 加 只更新变化的项
props 每次新引用 ❌ 别加 memo 拦不住,还多一次浅比较
组件渲染 < 1ms ❌ 别加 浅比较成本 > 渲染成本
children 每次都变 ❌ 别加 memo 拦不住
配合 useMemo/useCallback ✅ 加 props 引用稳定,memo 才有效
黄金组合:React.memo + useCallback/useMemo 要一起用才有效。单独加 memo 但 props 引用每次都变 = 没加。单独用 useCallback 但子组件没 memo = 没用。三者配合:memo 包子组件 + useCallback 包回调 + useMemo 包对象 = 才能真正跳过子组件渲染。

4. 状态管理选型

useState = 自己记笔记 记性够用就行
useReducer = 按流程表操作 逻辑复杂时按步骤来
Context = 公告栏 贴出来大家都能看到
Zustand = 微信群 轻量、自由、随取随用
Redux Toolkit = 政府公文系统 流程严格、可追溯、适合大项目
类比理解:状态管理就像信息传递方式的选择。useState 是自己记笔记(够用就行);useReducer 是按流程表操作(逻辑复杂时有条不紊);Context 是公告栏(贴出来大家都能看到);Zustand 是微信群(轻量、自由、随取随用);Redux Toolkit 是政府公文系统(流程严格、每一步可追溯、适合大型跨部门协作项目)。
① useState — 自己记笔记

适用:组件内部状态、简单值(string/number/boolean)、1-3 个相关状态

不适用:多组件共享、复杂状态逻辑、需要跨组件传递

📀 react-app
📁 src/components/
UseStateDemo.jsx
Counter.jsx
import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) const [step, setStep] = useState(1) return ( <div> <p>当前: {count}</p> <input type="number" value={step} onChange={e => setStep(Number(e.target.value))} /> <button onClick={() => setCount(c => c + step)}>+{step}</button> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
② useReducer — 按流程表操作

适用:状态逻辑复杂、多个子值互相依赖、下一个状态依赖前一个

不适用:简单独立状态(杀鸡用牛刀)

📀 react-app
📁 src/components/
UseReducerDemo.jsx
Counter.jsx
import { useReducer } from 'react' // 定义 reducer:状态变更逻辑集中管理 function cartReducer(state, action) { switch (action.type) { case 'ADD_ITEM': { const existing = state.items.find(i => i.id === action.payload.id) if (existing) { return { ...state, items: state.items.map(i => i.id === action.payload.id ? { ...i, quantity: i.quantity + 1 } : i ), } } return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }], } } case 'REMOVE_ITEM': return { ...state, items: state.items.filter(i => i.id !== action.payload) } case 'CLEAR_CART': return { ...state, items: [] } default: return state } } function ShoppingCart() { const [state, dispatch] = useReducer(cartReducer, { items: [] }) const total = state.items.reduce((sum, i) => sum + i.price * i.quantity, 0) return ( <div> <h2>购物车 ({state.items.length} 件)</h2> {state.items.map(item => ( <div key={item.id}> {item.name} x{item.quantity} = ¥{item.price * item.quantity} <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}> 删除 </button> </div> ))} <p>总计: ¥{total}</p> <button onClick={() => dispatch({ type: 'ADD_ITEM', payload: { id: Date.now(), name: '新商品', price: 99 } })}> 添加商品 </button> <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>清空</button> </div> ) }
面试金句 useReducer 是 useState 的进阶版,适合复杂状态逻辑(多状态联动、状态依赖前一个状态)。模式:(state, action) => newState。与 Redux 的关系:useReducer 是组件级的 reducer,Context + useReducer 可实现轻量级全局状态管理。
③ Context — 公告栏

适用:主题切换、语言设置、用户信息等"全局"但变更不频繁的数据

不适用:高频更新(如实时数据)、会导致所有消费者 re-render

📀 react-app
📁 src/context/
ThemeContext.jsx
UserContext.jsx
import { createContext, useContext, useState } from 'react' // 1. 创建 Context const ThemeContext = createContext() // 2. Provider 组件 function ThemeProvider({ children }) { const [theme, setTheme] = useState('light') const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light') } return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ) } // 3. 自定义 hook 方便使用 function useTheme() { const context = useContext(ThemeContext) if (!context) throw new Error('useTheme must be used within ThemeProvider') return context } // 4. 使用 function ThemedButton() { const { theme, toggleTheme } = useTheme() return ( <button onClick={toggleTheme} style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }} > 当前主题: {theme}(点击切换) </button> ) } // 5. 入口 function App() { return ( <ThemeProvider> <ThemedButton /> </ThemeProvider> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
④ Zustand — 微信群

适用:中等规模应用、需要跨组件共享状态、不想写太多 boilerplate

不适用:极简应用(杀鸡用牛刀)、需要严格的 action type 追踪(用 Redux)

📀 react-app
📁 src/stores/
useStore.js
package.json
// npm install zustand import { create } from 'zustand' // 创建 store:状态 + 操作写在一起 const useStore = create((set, get) => ({ // 状态 user: null, todos: [], // 操作 login: (userData) => set({ user: userData }), logout: () => set({ user: null }), addTodo: (text) => set(state => ({ todos: [...state.todos, { id: Date.now(), text, done: false }] })), toggleTodo: (id) => set(state => ({ todos: state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t ) })), // 计算属性可以用 getter getCompletedCount: () => get().todos.filter(t => t.done).length, })) // 使用:直接在任何组件里用,不需要 Provider function TodoList() { // ✅ 选择器:只订阅需要的状态,避免不必要 re-render const todos = useStore(state => state.todos) const addTodo = useStore(state => state.addTodo) const toggleTodo = useStore(state => state.toggleTodo) return ( <div> <button onClick={() => addTodo('新待办')}>添加</button> {todos.map(t => ( <div key={t.id} onClick={() => toggleTodo(t.id)}> {t.done ? '✓' : '○'} {t.text} </div> ))} </div> ) } function UserInfo() { const user = useStore(state => state.user) const login = useStore(state => state.login) if (!user) return <button onClick={() => login({ name: 'Alice' })}>登录</button> return <span>欢迎, {user.name}</span> }
面试金句 Zustand 用极简 API 实现全局状态管理:create() 创建 store,组件通过 hook 订阅需要的字段,只有订阅的字段变化才会 re-render。适合中等规模应用,比 Redux 轻量,比 Context 性能更好(不会全量 re-render)。

适用:中等规模应用、需要跨组件共享状态、不想写太多 boilerplate

不适用:极简应用(杀鸡用牛刀)、需要严格的 action type 追踪(用 Redux)

⑤ Redux Toolkit — 政府公文系统

适用:大型项目、多人协作、需要 DevTools 时间旅行调试、状态变更需要可追溯

不适用:小项目(过度工程)、简单状态(用 useState/Zustand 就够了)

📀 react-app
📁 src/features/
userSlice.js
store.js
// npm install @reduxjs/toolkit react-redux import { createSlice, configureStore } from '@reduxjs/toolkit' import { Provider, useSelector, useDispatch } from 'react-redux' // 1. 创建 slice const counterSlice = createSlice({ name: 'counter', initialState: { value: 0, history: [] }, reducers: { increment: (state) => { state.value += 1 state.history.push('+') }, decrement: (state) => { state.value -= 1 state.history.push('-') }, incrementByAmount: (state, action) => { state.value += action.payload state.history.push(`+${action.payload}`) }, }, }) const { increment, decrement, incrementByAmount } = counterSlice.actions // 2. 配置 store const store = configureStore({ reducer: { counter: counterSlice.reducer, }, }) // 3. 在组件中使用 function Counter() { const count = useSelector(state => state.counter.value) const history = useSelector(state => state.counter.history) const dispatch = useDispatch() return ( <div> <p>Count: {count}</p> <button onClick={() => dispatch(increment())}>+1</button> <button onClick={() => dispatch(decrement())}>-1</button> <button onClick={() => dispatch(incrementByAmount(10))}>+10</button> <p>操作记录: {history.join(', ')}</p> </div> ) } // 4. 入口 function App() { return ( <Provider store={store}> <Counter /> </Provider> ) }
面试金句 Redux 是 Flux 架构的实现:单一 Store + 纯函数 Reducer + Action 驱动。Redux Toolkit (RTK) 是现代标准用法,内置 immer(不可变更新)、createAsyncThunk(异步 action)。选型建议:中小项目用 Zustand,大型项目用 RTK。
决策表:状态管理方案选型
项目规模/需求 推荐方案 理由
单组件内部状态 useState 最简单,零配置
组件内复杂逻辑 useReducer 逻辑集中,可测试
跨组件共享主题/语言 Context React 内置,不需外部依赖
中等项目,跨组件多 Zustand 轻量、无 Provider、选择器
大型项目,多人协作 Redux Toolkit DevTools、中间件、可追溯
高频更新(实时数据) Zustand / useRef Context 会全量 re-render
服务端状态(API 缓存) React Query / SWR 专门处理请求/缓存/重试
核心原则:从简单开始,按需升级。能用 useState 解决就不要用 Context,能用 Context 解决就不要引入 Zustand,能用 Zustand 解决就不要上 Redux。每次升级都要问自己:当前方案哪里不够用了?新方案的收益是否大于引入的复杂度?

5. Suspense & Portal

Suspense = 电影缓冲条 加载中先看缓冲动画,加载完再看正片
Portal = 任意门 组件在 A 处定义,但渲染到 B 处的 DOM 里
类比理解:Suspense 就像看在线视频——遇到需要加载的内容,先显示缓冲条(fallback),加载完了再显示正片。Portal 就像哆啦A梦的任意门——组件虽然在组件树的 A 位置定义(保持上下文和事件),但 DOM 直接渲染到 B 位置(比如 body 末尾),解决 z-index 层叠和 overflow 裁切问题。
Suspense:React.lazy 代码分割

✅ 什么时候用:

路由级懒加载(每个页面独立 chunk)、重型组件按需加载(富文本编辑器、图表库)、统一 Loading 体验。

❌ 什么时候不用:

组件本身很小(分割反而增加请求开销)、首屏关键路径上的内容(不该懒加载首屏必需组件)、没有好的 fallback UI 时(白屏闪一下比 Loading 更糟)。

📀 react-app
📁 src/
App-lazy.jsx
routes.jsx
import { lazy, Suspense, useState } from 'react' // React.lazy: 动态 import,实现代码分割 const HeavyChart = lazy(() => import('./HeavyChart')) const RichEditor = lazy(() => import('./RichEditor')) const AdminPanel = lazy(() => import('./AdminPanel')) // Loading 组件 function Loading() { return ( <div style={{ textAlign: 'center', padding: 40 }}> <div className="spinner" /> <p>加载中...</p> </div> ) } function App() { const [tab, setTab] = useState('chart') return ( <div> <nav> <button onClick={() => setTab('chart')}>图表</button> <button onClick={() => setTab('editor')}>编辑器</button> <button onClick={() => setTab('admin')}>管理面板</button> </nav> // ✅ Suspense 包住懒加载组件,加载期间显示 fallback <Suspense fallback={<Loading />}> {tab === 'chart' && <HeavyChart />} {tab === 'editor' && <RichEditor />} {tab === 'admin' && <AdminPanel />} </Suspense> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
Portal:Modal 弹窗

✅ 什么时候用 Portal:

Modal 弹窗(解决 overflow:hidden 裁切)、Tooltip 提示框(解决 z-index 层叠问题)、全局通知/Toast、下拉菜单(父容器 overflow 裁切时)。

❌ 什么时候不用 Portal:

普通组件渲染(不需要脱离 DOM 层级)、简单弹窗(z-index 和 overflow 不是问题)、需要被父元素 CSS 影响的组件(Portal 渲染到别处了,继承不到父元素样式)。

📀 react-app
📁 src/components/
Modal-portal.jsx
Tooltip.jsx
import { createPortal, useState, useEffect } from 'react' // 通用 Modal 组件 function Modal({ isOpen, onClose, title, children }) { // ✅ 打开时阻止 body 滚动 useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden' } else { document.body.style.overflow = '' } return () => { document.body.style.overflow = '' } }, [isOpen]) if (!isOpen) return null // ✅ createPortal: 把 Modal 渲染到 body 末尾 // 这样即使父组件有 overflow:hidden 或 z-index 问题也不影响 return createPortal( <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999, }} onClick={onClose} // 点击遮罩关闭 > <div style={{ background: '#fff', padding: 24, borderRadius: 8, maxWidth: 500, width: '90%', }} onClick={e => e.stopPropagation()} // 阻止冒泡:点击内容不关闭 > <div style={{ display: 'flex', justifyContent: 'space-between' }}> <h2>{title}</h2> <button onClick={onClose}>×</button> </div> {children} </div> </div>, document.body // ← Portal 目标:渲染到 body 末尾 ) } // 使用示例 function App() { const [showModal, setShowModal] = useState(false) return ( // 即使这里有多层 overflow:hidden 的容器 // Modal 也不受影响,因为 Portal 直接渲染到 body <div style={{ overflow: 'hidden', height: 200 }}> <button onClick={() => setShowModal(true)}>打开弹窗</button> <Modal isOpen={showModal} onClose={() => setShowModal(false)} title="确认操作" > <p>确定要执行这个操作吗?</p> <button onClick={() => setShowModal(false)}>取消</button> <button onClick={() => { /* 执行操作 */ setShowModal(false) }}>确认</button> </Modal> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
决策表:Suspense & Portal 使用场景
技术 场景 ✅ 用 ❌ 不用
Suspense + lazy 路由级懒加载 每个页面独立 chunk 首屏关键组件
Suspense + lazy 重型组件按需加载 编辑器/图表等大组件 几KB的小组件
Portal Modal 弹窗 overflow/z-index 问题 简单浮层无遮挡
Portal Tooltip 父容器 overflow 裁切 普通定位即可
Portal 全局 Toast 统一位置管理 局部提示
关键注意:Suspense 的 fallback 在子组件加载时显示,但如果多个懒加载组件在同一个 Suspense 内,任一未就绪都会显示 fallback。可以嵌套 Suspense 实现更精细的 Loading 控制。Portal 的事件冒泡仍然按 React 组件树传播(不是 DOM 树),所以 Portal 内的 onClick 仍然能被父组件捕获。

6. SSR vs CSR vs SSG

CSR = 客户端渲染 浏览器下载JS后才生成内容,首屏稍慢
SSR = 服务端渲染 服务器直接返回HTML,首屏快,动态内容实时生成
SSG = 静态生成 构建时预渲染好HTML,访问直接返回,最快但内容固定
核心机制:CSR(客户端渲染)——浏览器先下载空白HTML+JS,执行JS后才渲染页面内容,首屏依赖JS下载和执行速度。SSR(服务端渲染)——服务器执行React组件生成完整HTML返回给浏览器,首屏直接可见,同时JS hydrate后接管交互。SSG(静态生成)——在构建时(npm run build)就把页面渲染成HTML文件,访问时直接返回静态文件,速度最快但内容在构建时就确定了,适合博客/文档等不常变的页面。
CSR — 客户端渲染

✅ 适用:交互密集型应用、后台管理系统、不需要 SEO 的内部工具、实时仪表盘

❌ 不适用:需要 SEO 的内容站、首屏速度要求高、弱网环境

流程:浏览器下载空 HTML → 下载 JS bundle → 执行 JS → 渲染页面。首屏慢但后续交互快。

SSR — 服务端渲染

✅ 适用:需要 SEO 的电商/博客、首屏速度要求高、社交媒体分享预览

❌ 不适用:纯交互应用(白屏反而更慢)、服务器资源有限(每次请求都要渲染)

流程:用户请求 → 服务器执行 React → 生成完整 HTML → 返回给浏览器。首屏快但服务器压力大。

SSG — 静态站点生成

✅ 适用:博客、文档站、落地页、产品介绍页、内容不频繁变化

❌ 不适用:内容实时变化(股票行情)、用户个性化内容(推荐信息流)

流程:构建时生成所有 HTML → 部署到 CDN → 用户访问直接返回静态文件。最快、最便宜。

代码示例:Next.js 三种渲染模式
📀 react-app
📁 src/components/
ErrorBoundary.jsx
App.jsx
// ============================================= // 1. CSR — 客户端渲染(use client) // 文件: app/dashboard/page.js // ============================================= 'use client' import { useState, useEffect } from 'react' export default function Dashboard() { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { // CSR:在浏览器端请求数据 fetch('/api/dashboard') .then(res => res.json()) .then(data => { setData(data); setLoading(false) }) }, []) if (loading) return <div>加载中...</div> return ( <div> <h1>仪表盘</h1> <p>欢迎回来, {data.user.name}</p> {/* 交互密集的仪表盘内容 */} </div> ) } // ============================================= // 2. SSR — 服务端渲染 // 文件: app/products/page.js // ============================================= // Next.js App Router 默认就是服务端组件 // 这个页面每次请求都在服务器端渲染 export default async function Products() { // ✅ SSR:直接在服务端 fetch 数据 // 用户拿到的 HTML 已经包含完整的产品列表 const res = await fetch('https://api.example.com/products', { cache: 'no-store', // 每次请求都重新获取 = SSR }) const products = await res.json() return ( <div> <h1>商品列表</h1> <ul> {products.map(p => ( <li key={p.id}> <h2>{p.name}</h2> <p>¥{p.price}</p> </li> ))} </ul> </div> ) } // ============================================= // 3. SSG — 静态站点生成 // 文件: app/blog/page.js // ============================================= // 构建时就生成 HTML,不需要服务器运行时 export default async function Blog() { const res = await fetch('https://api.example.com/posts', { cache: 'force-cache', // 构建时缓存 = SSG }) const posts = await res.json() return ( <div> <h1>博客文章</h1> {posts.map(post => ( <article key={post.id}> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article> ))} </div> ) } // ============================================= // ISR — 增量静态再生成(SSG + 定期更新) // ============================================= const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 }, // 每 3600 秒重新生成 = ISR })
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
决策表:CSR vs SSR vs SSG
维度 CSR SSR SSG
首屏速度 慢(等JS下载执行) 快(直接返回HTML) 最快(CDN直返)
SEO 差(空HTML) 好(完整HTML) 好(完整HTML)
服务器压力 低(静态托管) 高(每次渲染) 最低(构建一次)
内容实时性 好(客户端获取) 好(服务端获取) 差(构建时固定)
适用场景 后台管理/内部工具 电商/社交/新闻 博客/文档/落地页
类比 下载后才执行(等JS) 服务器生成HTML(首屏快) 构建时生成(最快)
Next.js 混合渲染:真实项目中不是三选一,而是混合使用——首页用 SSG(最快)、商品页用 SSR(SEO + 实时)、管理后台用 CSR(交互优先)。Next.js App Router 让你按路由/组件粒度选择渲染策略。ISR(增量静态再生成)是 SSG 的增强版——静态页面的性能 + 定期更新的新鲜度。

7. useTransition & useDeferredValue

useTransition = 后台编译 前台继续操作,后台默默编译
useDeferredValue = 延迟加载图片 先看文字,图片慢慢加载出来
类比理解:useTransition 就像 IDE 的后台编译——你改了代码,输入框不卡,IDE 在后台默默编译。编译完了自动更新结果,编译中显示一个"处理中"的状态。useDeferredValue 就像网页的懒加载图片——你先看到文字内容,图片在后台慢慢加载出来,不会因为图片多就卡住整个页面。两者本质上都是让 React 区分"紧急更新"和"非紧急更新",优先保证用户输入的流畅。
useTransition — 标记非紧急更新

✅ 适用:搜索过滤大列表、Tab 切换渲染重型内容、任何"用户输入 → 昂贵渲染"的场景

❌ 不适用:输入框本身的状态更新(那是紧急的)、简单的无感操作(不需要优先级调度)

核心思想:把昂贵的状态更新标记为"非紧急"(transition),React 会在空闲时处理它们,不会阻塞用户输入。

useDeferredValue — 延迟渲染昂贵内容

✅ 适用:搜索结果延迟渲染、大数据表格延迟更新、不需要立即响应的内容

❌ 不适用:必须立即反映的内容(如输入框的值)、简单的无感更新

和 useTransition 的区别:useTransition 让你"延迟 setState",useDeferredValue 让你"延迟读取 state"。效果类似,API 不同。

代码示例:useTransition 搜索过滤
📀 react-app
📁 src/components/
UseTransitionDemo.jsx
SearchResults.jsx
import { useState, useTransition, memo } from 'react' // 模拟大数据 const allItems = Array.from({ length: 20000 }, (_, i) => ({ id: i, text: `项目 ${String(i).padStart(5, '0')}`, })) // 用 memo 包裹列表项,配合 useTransition 更高效 const ListItem = memo(function ListItem({ text }) { return <div>{text}</div> }) function SearchableList() { const [inputValue, setInputValue] = useState('') const [searchQuery, setSearchQuery] = useState('') const [isPending, startTransition] = useTransition() // 输入变化:立即更新输入框(紧急) // 过滤列表:延迟处理(非紧急) const handleSearch = (e) => { const value = e.target.value // ✅ 紧急更新:输入框立即响应 setInputValue(value) // ✅ 非紧急更新:过滤 20000 条数据 // React 会在空闲时处理,不会卡住输入框 startTransition(() => { setSearchQuery(value) }) } // 只有 searchQuery 变了才重新过滤 const filteredItems = searchQuery ? allItems.filter(item => item.text.toLowerCase().includes(searchQuery.toLowerCase()) ) : allItems return ( <div> <input type="text" value={inputValue} onChange={handleSearch} placeholder="搜索 20000 条数据..." /> {isPending && <span> 过滤中...</span>} <p>共 {filteredItems.length} 条结果</p> <div style={{ height: 400, overflow: 'auto' }}> {filteredItems.slice(0, 100).map(item => ( <ListItem key={item.id} text={item.text} /> ))} </div> </div> ) }
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
代码示例:useDeferredValue 延迟渲染大列表
📀 react-app
📁 src/components/
UseDeferredValue.jsx
SearchResults.jsx
import { useState, useDeferredValue, useMemo } from 'react' function DeferredSearchList() { const [query, setQuery] = useState('') // ✅ useDeferredValue: 延迟版的 query // query 立即更新(输入框流畅) // deferredQuery 在空闲时才更新(列表延迟渲染) const deferredQuery = useDeferredValue(query) // 用延迟值做过滤 const filteredItems = useMemo(() => { if (!deferredQuery) return allItems return allItems.filter(item => item.text.toLowerCase().includes(deferredQuery.toLowerCase()) ) }, [deferredQuery]) // 判断是否在延迟中(query 和 deferredQuery 不同步) const isStale = query !== deferredQuery return ( <div> // ✅ 输入框绑定原始 query,始终流畅 <input value={query} onChange={e => setQuery(e.target.value)} placeholder="搜索..." /> {isStale && <span style={{ color: '#888' }}>更新中...</span>} // ✅ 列表用延迟值,不影响输入框流畅度 <div style={{ opacity: isStale ? 0.7 : 1 }}> {filteredItems.slice(0, 50).map(item => ( <div key={item.id}>{item.text}</div> ))} </div> </div> ) } // 对比 useTransition vs useDeferredValue: // useTransition: 你控制什么时候 startTransition(() => setState(...)) // useDeferredValue: React 自动延迟 state 的值 // // 什么时候用哪个? // - 如果你能控制 setState → 用 useTransition(更灵活,有 isPending) // - 如果 state 来自 props 或第三方 → 用 useDeferredValue(不需要改 setState) // - 效果几乎相同:都是让昂贵的渲染让步于用户输入
面试金句 useState 是 React 最基础的 Hook,用于管理函数组件的局部状态。关键点:setState 是异步批量的(React 18 自动批处理),连续调用 setState 会被合并。函数式更新(prev => prev + 1)确保基于最新状态计算,避免闭包陷阱。
决策表:useTransition vs useDeferredValue
维度 useTransition useDeferredValue
控制方式 包裹 setState 包裹 state 值
有 isPending 状态 ✅ 有 ❌ 没有(手动判断)
适用:控制 setState ✅ 更灵活
适用:state 来自外部 ✅ 不需要改 setState
类比 后台编译(你主动触发) 延迟加载图片(自动延迟)
共同目标 昂贵的渲染让步于用户输入,保持 UI 流畅
核心原则:useTransition 和 useDeferredValue 都是 React 18 并发特性的核心。它们不是性能优化工具(不会让渲染更快),而是优先级调度工具(让重要的更新先执行,不重要的后执行)。如果你的搜索过滤本身很快(< 16ms),完全不需要这两个 hook——直接用 useState 就行。只有当渲染耗时导致输入卡顿时,才需要引入优先级调度。
警告:不要滥用 useTransition——如果把所有 setState 都包进 startTransition,等于没有优先级区分。只把"昂贵但不紧急"的更新标记为 transition(如搜索结果渲染、Tab 内容切换)。输入框的值、开关状态等用户直接感知的更新必须是紧急的(普通 setState)。

React 场景决策 — 7 大核心场景全解析

useMemo vs useCallback | useEffect | React.memo | 状态管理 | Suspense & Portal | SSR vs CSR vs SSG | useTransition