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 过滤大列表
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
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 变化执行逻辑,先想想能不能在事件处理函数中做。
代码示例:轮询请求 + 清理
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)确保基于最新状态计算,避免闭包陷阱。
代码示例:事件监听 + 清理 + 依赖
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)确保基于最新状态计算,避免闭包陷阱。
代码示例:对象依赖导致无限循环 & 修复
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 的引用每次都在变。
代码示例:列表项优化
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)
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 个相关状态
不适用:多组件共享、复杂状态逻辑、需要跨组件传递
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 — 按流程表操作
适用:状态逻辑复杂、多个子值互相依赖、下一个状态依赖前一个
不适用:简单独立状态(杀鸡用牛刀)
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
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)
// 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 就够了)
// 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 更糟)。
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 渲染到别处了,继承不到父元素样式)。
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 三种渲染模式
// =============================================
// 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 搜索过滤
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 延迟渲染大列表
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)。