React 高级开发 & 原理深耕 - 资深架构师实战指南
目录
一、React 底层原理
1.1 Fiber 架构深度解析
1.1.1 为什么需要 Fiber?(从 Stack Reconciler 到 Fiber Reconciler)
【架构演进】Fiber 是 React 16 最重要的架构升级,解决了同步渲染的阻塞问题
传统 Stack Reconciler(React 15 及之前):
┌─────────────────────────────────────┐
│ 同步渲染:不可中断 │
│ │
│ 用户点击 → setState → 完整渲染 → 绘制 │
│ ↓ │
│ 组件树深度遍历 │
│ (可能耗时 100ms+) │
│ ↓ │
│ 【阻塞主线程,用户感到卡顿】 │
└─────────────────────────────────────┘
Fiber Reconciler(React 16+):
┌─────────────────────────────────────┐
│ 异步可中断渲染 │
│ │
│ 用户点击 → setState → 渲染(可中断) │
│ ↓ │
│ 分片执行,每帧 16ms │
│ 高优先级任务可打断 │
│ ↓ │
│ 【保持流畅,用户交互不卡顿】 │
└─────────────────────────────────────┘
🔄 Fiber 的核心思想
// Fiber 节点数据结构(简化版)
const fiber = {
// 节点标识
type: 'div', // 组件类型
key: null, // React key
stateNode: DOMElement, // 真实 DOM 引用
// 链表结构(Fiber 的核心)
child: fiber, // 第一个子节点
sibling: fiber, // 下一个兄弟节点
return: fiber, // 父节点
// 渲染所需数据
pendingProps: {}, // 新的 props
memoizedProps: {}, // 缓存的 props
memoizedState: {}, // 组件状态
// 更新队列
updateQueue: [], // 待处理的更新
lanes: 0, // 优先级标记
// 副作用标记
flags: 0, // 副作用类型
subtreeFlags: 0, // 子树副作用
};
📊 Fiber 链表遍历算法
// 深度优先遍历(DFS)改为【顺序遍历 + 可中断】
function beginWork(wip) {
// 1. 创建子 Fiber 节点
// 2. 根据 props/state 更新组件
// 3. 返回第一个子节点继续处理
switch (wip.tag) {
case HostRoot:
return updateHostRoot(wip);
case HostComponent:
return updateHostComponent(wip);
case FunctionComponent:
return updateFunctionComponent(wip);
// ...
}
}
// 可中断的工作循环
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
// shouldYield() 检查是否需要让出主线程
function shouldYield() {
return getCurrentTime() >= deadline;
}
1.1.2 优先级与 Lane 模型
【性能关键】React 18 使用 Lane(车道)模型替代之前的优先级概念
// React 18 Lane 模型(简化)
const lanes = {
// 同步车道
SyncLane: 0b0001,
// 连续交互车道(用户拖拽、输入等)
InputContinuousLane: 0b0010,
// 默认车道(数据获取、动画等)
DefaultLane: 0b0100,
// 过渡车道(startTransition 标记的任务)
TransitionLane1: 0b1000,
TransitionLane2: 0b10000,
// 空闲车道(预加载等低优先级任务)
IdleLane: 0b100000,
};
// 优先级排序(从高到低)
// SyncLane > InputContinuousLane > DefaultLane > TransitionLane > IdleLane
🎯 startTransition 的原理
// React 18 startTransition API
const App = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleSearch = (value) => {
setQuery(value); // 紧急更新:立即响应
// startTransition 标记的更新是【可中断的】
startTransition(() => {
// 这个更新可以被高优先级任务打断
setResults(expensiveSearch(value));
});
};
return (
<div>
<input value={query} onChange={(e) => handleSearch(e.target.value)} />
<SearchResults results={results} />
</div>
);
};
1.1.3 Concurrent Mode(并发模式)
【React 18 新特性】并发渲染使得多个状态更新可以同时进行
// React 18 自动批处理(Automatic Batching)
// 之前:每个 setState 都触发一次重新渲染
// 现在:多个 setState 合并为一次渲染
const BadExample = () => {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const handleClick = () => {
fetchSomething().then(() => {
// ❌ React 17 中这会触发 2 次渲染
// ✅ React 18 中自动批处理,只触发 1 次渲染
setCount(c => c + 1);
setFlag(f => !f);
});
};
return <button onClick={handleClick}>Click</button>;
};
// useTransition vs useDeferredValue
const SearchResults = ({ query }) => {
// useTransition:标记非紧急更新
const [isPending, startTransition] = useTransition();
const [deferredQuery, setDeferredQuery] = useState(query);
// useDeferredValue:延迟更新(类似防抖但更智能)
const deferredQuery = useDeferredValue(query);
return (
<div style={{ opacity: isPending ? 0.5 : 1 }}>
<Results query={deferredQuery} />
</div>
);
};
1.2 调和算法(Reconciliation)
1.2.1 调和算法核心思想
【原理】调和是 React 用来对比新旧 Virtual DOM 的算法,决定如何高效更新真实 DOM
┌─────────────────────────────────────────────────────┐
│ │
│ setState() ──→ 生成新 Virtual DOM │
│ ↓ │
│ 【Reconciliation】 │
│ ↓ │
│ 对比新旧 Virtual DOM(Diff 算法) │
│ ↓ │
│ 计算出最小更新操作 │
│ ↓ │
│ 【Commit 阶段】更新真实 DOM │
│ │
└─────────────────────────────────────────────────────┘
1.2.2 Diff 算法的三个核心假设
【性能关键】这三条假设使得 O(n³) → O(n) 的复杂度成为可能
假设一:不同类型的元素产生不同的树
// 当元素类型不同时,React 会【完全替换】整棵子树
// 不再比较子节点,直接销毁旧树,创建新树
// 旧树
<div className="before">
<ComponentA />
<ComponentB />
</div>
// 新树(类型不同,整棵子树被替换)
<span className="after">
<ComponentA />
<ComponentB />
</span>
// React 行为:
// 1. 卸载 <div> 及其所有子组件(触发 componentWillUnmount)
// 2. 创建新的 <span> 和子组件(触发 componentDidMount)
// 【性能开销】很大,但保证了正确性
假设二:通过 key 来标识同级兄弟元素
// ✅ 好的 key:稳定、唯一、可预测
const GoodList = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li> // 使用数据库 ID
))}
</ul>
);
// ❌ 坏的 key:索引、随机值
const BadList = ({ users }) => (
<ul>
{users.map((user, index) => (
<li key={index}>{user.name}</li> // 插入/删除会导致错误匹配
))}
</ul>
);
// ⚠️ 使用 Math.random() 作为 key 是最糟糕的做法
// 每次渲染 key 都不同,React 会认为所有元素都是新的
const TerribleList = ({ items }) => (
<ul>
{items.map(item => (
<li key={Math.random()}>{item.name}</li> // 💀 永远不要这样做
))}
</ul>
);
假设三:只同层对比,不跨层级移动
旧树: 新树:
<div> <div>
<p> <p>
<span>A</span> → <span>A</span> ✅ 同位置,保留
</p> </p>
<p> <p>
<span>B</span> → <span>C</span> ⚠️ 同位置但内容变了,更新
</p> </p>
<p>
<span>D</span> ✨ 新增
</p>
</div> </div>
// React 不会尝试跨层级移动元素
// 如果要移动,需要完全卸载并重新挂载
1.2.3 Diff 算法源码解析
// React 源码简化版:reconcileChildrenArray
function reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChildren
) {
// 1. 首先遍历新数组,为每个元素创建或复用 Fiber
let resultingFirstChild = null;
let prevDiffFiber = null;
let newIdx = 0;
// 第一轮:尝试复用已有的 Fiber(基于 key)
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createFiber(newChildren[newIdx], returnFiber);
if (newFiber.key === null) {
// 无 key 的元素,尝试用索引匹配
// ⚠️ 这就是为什么无 key 列表性能较差
}
// 复用或创建 Fiber
const matchedFiber = reconcileChildFibers(
returnFiber,
currentFirstChild,
newFiber
);
if (matchedFiber === null) break;
// 更新链表
if (prevDiffFiber === null) {
resultingFirstChild = matchedFiber;
} else {
prevDiffFiber.sibling = matchedFiber;
}
prevDiffFiber = matchedFiber;
}
// 2. 处理剩余的旧 Fiber(需要删除)
if (oldFiber !== null) {
deleteChild(returnFiber, oldFiber);
}
return resultingFirstChild;
}
1.3 Hooks 底层实现原理
1.3.1 Hooks 链表设计
【核心原理】React 通过链表保存 Hooks 的状态,每个 Hook 节点包含 [state, setState]
// 函数组件的 Hooks 链表结构
const FunctionComponentFiber = {
// ... 其他字段
// 【关键】保存 Hooks 状态的链表
memoizedState: {
// Hook 1: useState
baseState: 0, // 基础状态值
queue: { pending: null }, // 更新队列
next: {
// Hook 2: useState
baseState: '',
queue: { pending: null },
next: {
// Hook 3: useEffect
baseState: null,
queue: { pending: null },
next: null // 链表结束
}
}
}
};
// 首次渲染:按顺序创建链表
function useState(initialState) {
// 按顺序获取当前 hook(首次创建,复用时读取)
const hook = workInProgressHook();
if (workInProgressHook === null) {
// 首次渲染:初始化
hook.baseState = typeof initialState === 'function'
? initialState()
: initialState;
}
// 返回 [state, setState]
return [hook.baseState, dispatchAction.bind(null, hook.queue)];
}
// 更新渲染:遍历链表获取已有状态
function dispatchAction(hook.queue, action) {
// 创建更新对象,加入队列
const update = { action, next: null };
// ... 合并到环形链表
// 标记需要重新渲染
scheduleUpdateOnFiber(currentFiber);
}
1.3.2 useState 完整实现解析
// React 源码:useState 实现
function useState(initialState) {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
hook.memoizedState = initialState();
} else {
hook.memoizedState = initialState;
}
const queue = {
pending: null,
dispatch: dispatchSetState.bind(null, currentlyRenderingFiber, queue),
};
hook.queue = queue;
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
// dispatchSetState:更新状态
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
const update = {
action,
lane,
next: null,
};
// 加入环形队列
enqueueUpdate(fiber, queue, update);
// 调度更新(异步可中断)
scheduleUpdateOnFiber(fiber, lane);
}
// 状态计算:使用环形链表合并所有更新
function updateReducer(reducer, initialState) {
const hook = workInProgressHook;
let newState = hook.memoizedState;
if (hook.baseQueue !== null) {
const firstBaseUpdate = hook.baseQueue.next;
let newBaseUpdate = firstBaseUpdate;
do {
// 处理每个更新
if (newBaseUpdate.action instanceof Function) {
newState = newBaseUpdate.action(newState);
} else {
newState = newBaseUpdate.action;
}
newBaseUpdate = newBaseUpdate.next;
} while (newBaseUpdate !== firstBaseUpdate);
}
hook.memoizedState = newState;
return [newState, hook.queue.dispatch];
}
1.3.3 useEffect 的实现原理
// React 源码:useEffect 实现
function useEffect(create, deps) {
const hook = mountWorkInProgressHook();
let nextDeps = deps === undefined ? null : deps;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
// 【关键】依赖比较:浅比较
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖没变,跳过此次 effect
return;
}
}
// 依赖变了,添加 effect 到链表
hook.memoizedState = pushEffect(
HookHasEffect | HookPassive, // flags
create,
nextDeps
);
}
// pushEffect:创建副作用节点
function pushEffect(tag, create, deps) {
const effect = {
tag,
create,
deps,
next: null,
};
if (componentUpdateQueue === null) {
componentUpdateQueue = { lastEffect: null };
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
effect.next = lastEffect.next;
lastEffect.next = effect;
componentUpdateQueue.lastEffect = effect;
}
return effect;
}
// 提交阶段:执行所有 effects
function commitPassiveEffects(root, lanes) {
// 遍历所有 Passive effect
// 调用 create() 返回的清理函数
// 调用新的 create()
}
1.3.4 闭包陷阱与依赖项规则
【高频面试点】理解闭包陷阱才能正确使用 useEffect
⚠️ 经典闭包陷阱案例
// ❌ 陷阱 1:setInterval 中的闭包
const BadTimer = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 永远是首次渲染的值(0)!
}, 1000);
return () => clearInterval(id);
}, []); // 空依赖,effect 只执行一次
return <div>{count}</div>; // 永远只会显示 1
};
// ✅ 解决方案 1:函数式更新
const GoodTimer1 = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // 使用函数式更新,始终基于最新值
}, 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
// ✅ 解决方案 2:使用 useRef
const GoodTimer2 = () => {
const [count, setCount] = useState(0);
const countRef = useRef(0); // 可变引用,不触发重新渲染
useEffect(() => {
const id = setInterval(() => {
countRef.current += 1;
setCount(countRef.current);
}, 1000);
return () => clearInterval(id);
}, []);
return <div>{count}</div>;
};
// ❌ 陷阱 2:useEffect 依赖遗漏
const BadEffect = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
// 使用了 userId 但未加入依赖
fetchUser(userId).then(setUser);
}, []); // ⚠️ 缺少 [userId]!
// userId 变化时不会重新获取数据
return <div>{user?.name}</div>;
};
// ✅ 正确写法
const GoodEffect = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
let isCancelled = false; // 防止卸载后 setState
fetchUser(userId).then(data => {
if (!isCancelled) setUser(data);
});
return () => {
isCancelled = true; // 清理函数
};
}, [userId]); // ✅ 包含所有使用的外部变量
return <div>{user?.name}</div>;
};
🔧 ESLint exhaustive-deps 规则
// .eslintrc 配置
{
"rules": {
"react-hooks/exhaustive-deps": [
"error",
{
"additionalHooks": "(useMyCustomHook)" // 注册自定义 hooks
}
]
}
}
// ⚠️ 这个规则会自动检测所有 useEffect 的依赖
// 必须严格遵守,否则可能出现:
// 1. 数据过时(stale data)
// 2. 内存泄漏
// 3. 无限循环
1.4 合成事件机制
1.4.1 合成事件 vs 原生事件
【架构设计】React 为了跨平台和性能,设计了合成事件系统
┌─────────────────────────────────────────────────────┐
│ │
│ 原生事件(Native Events) │
│ ├── click ── DOM Level 3 Events │
│ ├── focus ── FocusEvent │
│ ├── submit ── Event │
│ └── touchstart ── TouchEvent │
│ │
│ 合成事件(SyntheticEvent) │
│ ├── onClick ── SyntheticEvent │
│ ├── onFocus ── SyntheticFocusEvent │
│ ├── onSubmit ── SyntheticEvent │
│ └── onTouchStart ── SyntheticTouchEvent │
│ │
└─────────────────────────────────────────────────────┘
1.4.2 事件绑定原理
// React 17+ 的事件委托架构
const root = document.getElementById('root');
// React 16:所有事件绑定到根容器
root.attachEventListener('onclick', ReactEventListener);
// React 17+:所有事件绑定到根 DOM 节点
// 好处:多个 React 版本共存时事件不冲突
root.addEventListener('click', ReactEventListener, false);
// ReactEventListener 内部
function ReactEventListener() {
// 1. 从事件源向上遍历 DOM 树
// 2. 收集所有绑定了同一事件的 React 组件
// 3. 按照【冒泡/捕获】顺序执行
// 4. 构建 synthetic event 传递给每个组件
}
// React 给 DOM 元素添加的所有事件监听
const nativeEventToReactEvent = {
'click': 'onClick',
'mousedown': 'onMouseDown',
'touchstart': 'onTouchStart',
'change': 'onChange', // 特殊处理
'input': 'onInput', // 特殊处理
};
1.4.3 事件池与性能优化
// React 17 之前:事件对象被【池化】复用
// 事件处理完成后,对象被归还到池中,属性被清空
// ❌ 这段代码在 React 16 有问题
const BadComponent = () => {
const handleClick = (e) => {
setTimeout(() => {
console.log(e.target.value); // ⚠️ 事件对象已被回收!
}, 0);
};
return <button onClick={handleClick}>Click</button>;
};
// ✅ React 17+ 不再使用事件池
// 事件对象会一直保留,直到所有异步操作完成
const GoodComponent = () => {
const handleClick = (e) => {
const value = e.target.value; // ✅ 安全地保存值
setTimeout(() => {
console.log(value);
}, 0);
};
return <button onClick={handleClick}>Click</button>;
};
1.4.4 常见事件问题处理
// 1. 事件冒泡与阻止
const EventHandling = () => {
const handleChildClick = (e) => {
e.stopPropagation(); // ✅ 阻止冒泡
console.log('Child clicked');
};
const handleParentClick = () => {
console.log('Parent clicked'); // 不会触发
};
return (
<div onClick={handleParentClick}>
<button onClick={handleChildClick}>Click me</button>
</div>
);
};
// 2. 事件捕获(从外到内)
const CaptureEvent = () => (
<div onClickCapture={handleOuter}>
<div onClickCapture={handleMiddle}>
<button onClick={handleInner}>Click</button>
</div>
</div>
);
// 点击按钮时执行顺序:handleOuter → handleMiddle → handleInner
// 3. 键盘事件
const KeyboardInput = () => {
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
console.log('Enter pressed!');
}
if (e.ctrlKey && e.key === 's') {
e.preventDefault(); // 阻止默认行为(保存快捷键)
saveDocument();
}
};
return (
<input
onKeyDown={handleKeyDown}
placeholder="Press Enter or Ctrl+S"
/>
);
};
// 4. 表单事件(与原生行为的区别)
const FormEvents = () => {
const handleSubmit = (e) => {
e.preventDefault(); // ✅ React 中必须手动阻止默认行为
};
const handleChange = (e) => {
const { name, value } = e.target; // ✅ 获取表单值
console.log(`${name}: ${value}`);
};
return (
<form onSubmit={handleSubmit}>
<input name="username" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
);
};
二、高级 Hooks & 组件复用
2.1 进阶 Hooks 深度应用
2.1.1 useMemo —— 计算结果缓存
【性能优化】避免每次渲染都重新计算昂贵的数据
// ✅ 场景 1:避免重复计算
const ExpensiveCalculation = ({ data, filter }) => {
// ❌ 每次渲染都重新计算(data 变化时)
const processedData = data
.filter(item => item.type === filter)
.sort((a, b) => a.value - b.value)
.map(item => ({ ...item, computed: item.value * 1.5 });
// ✅ 使用 useMemo:仅在 data 或 filter 变化时重新计算
const processedData = useMemo(() => {
console.log('Running expensive calculation...'); // 用于观察
return data
.filter(item => item.type === filter)
.sort((a, b) => a.value - b.value)
.map(item => ({ ...item, computed: item.value * 1.5 }));
}, [data, filter]); // 依赖数组
return <DataList data={processedData} />;
};
// ✅ 场景 2:稳定对象引用(避免子组件重渲染)
const Parent = ({ a, b }) => {
const [count, setCount] = useState(0);
// ❌ 每次渲染都创建新对象,导致 MemoizedChild 重渲染
const config = { padding: 10, margin: a };
// ✅ useMemo 缓存对象引用
const config = useMemo(() => ({
padding: 10,
margin: a,
}), [a]);
return (
<>
<MemoizedChild config={config} />
<button onClick={() => setCount(c => c + 1)}>Re-render: {count}</button>
</>
);
};
const MemoizedChild = React.memo(({ config }) => {
console.log('MemoizedChild rendered'); // 观察重渲染
return <div style={config}>{config.margin}</div>;
});
// ⚠️ 不要过度使用 useMemo:
// - 没有明显的性能问题时,useMemo 可能反而增加开销
// - 简单的计算(如 a + b)不需要 useMemo
// - 依赖数组为空时等价于 componentDidMount(基本不用)
2.1.2 useCallback —— 回调函数缓存
【最佳拍档】与 React.memo 配合使用,防止不必要的重渲染
// ✅ useCallback = useMemo + 函数封装
// useCallback(fn, deps) 等价于 useMemo(() => fn, deps)
// 场景:传递给子组件的回调函数
const Parent = ({ userId }) => {
const [data, setData] = useState(null);
// ❌ 每次渲染都创建新函数,子组件会重渲染
const handleSelect = (item) => {
console.log('Selected:', item);
setData(item);
};
// ✅ useCallback 缓存函数引用
const handleSelect = useCallback((item) => {
console.log('Selected:', item);
setData(item);
}, []); // 依赖为空,函数引用始终稳定
// ✅ 结合 React.memo 实现精准渲染控制
return <List items={items} onSelect={handleSelect} />;
};
const List = React.memo(({ items, onSelect }) => {
console.log('List rendered');
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item)}>
{item.name}
</li>
))}
</ul>
);
});
// ⚠️ useCallback 也有开销,不要滥用
// 简单场景下可能适得其反
const SimpleButton = ({ onClick }) => {
// ❌ 过度优化:普通按钮组件不需要 useCallback
const handleClick = useCallback(() => {
onClick();
}, [onClick]);
return <button onClick={handleClick}>Click</button>;
};
📊 useMemo vs useCallback 对比
| Hook | 缓存内容 | 典型场景 | 注意事项 |
|---|---|---|---|
| useMemo | 计算结果(值) | 昂贵计算、对象/数组引用 | 不要用于简单计算 |
| useCallback | 函数引用 | 传递给子组件的回调 | 配合 React.memo 使用 |
2.1.3 useLayoutEffect —— 同步执行副作用
【使用场景】DOM 操作后立即同步更新视觉
// ⚠️ 与 useEffect 的区别
// useEffect:异步执行,渲染后再执行(可能导致闪烁)
// useLayoutEffect:同步执行,DOM 变更后立即执行(阻塞绘制)
// ✅ useLayoutEffect 场景:获取 DOM 尺寸、焦点管理、滚动位置
const Tooltip = ({ targetRef, children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
useLayoutEffect(() => {
// ✅ DOM 已经更新,但还未绘制
// 同步计算位置,避免闪烁
const rect = targetRef.current.getBoundingClientRect();
setPosition({
x: rect.right,
y: rect.top,
});
}, [targetRef]);
return (
<div style={{ position: 'absolute', left: position.x, top: position.y }}>
{children}
</div>
);
};
// ✅ useEffect 场景:数据获取、订阅
const DataFetcher = ({ url }) => {
const [data, setData] = useState(null);
useEffect(() => {
// 异步获取数据,不阻塞渲染
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
};
// ⚠️ SSR 时的警告
const Component = () => {
useLayoutEffect(() => {
// React 17 及之前:SSR 时会报错
// React 18+:会在 useEffect 之前执行,但仍需注意
}, []);
return <div>Content</div>;
};
// ✅ 解决方案:添加防闪烁处理
const SSR-safeComponent = ({ serverData }) => {
const [clientData, setClientData] = useState(null);
useLayoutEffect(() => {
// 仅在客户端执行
setClientData(computeClientSide(serverData));
}, [serverData]);
return <div>{clientData ?? serverData}</div>; // 避免 hydration 不匹配
};
2.1.4 useId —— 生成稳定唯一 ID
【React 18 新增】解决 SSR/Hydration 时 ID 不稳定的问题
// ❌ 旧方案:使用 Math.random() 或 index(SSR 不稳定)
const OldInput = ({ label }) => {
const id = Math.random().toString(36); // 💀 每次渲染都不同!
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
};
// ✅ 新方案:useId(React 18+)
const NewInput = ({ label }) => {
const id = useId(); // ✅ 稳定、唯一、在 SSR 中也能保持一致
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} />
</div>
);
};
// ✅ 多个 ID 的场景(使用共享前缀)
const MultipleInputs = () => {
const id = useId();
return (
<div>
<input id={`${id}-name`} />
<input id={`${id}-email`} />
<input id={`${id}-phone`} />
</div>
);
};
// ✅ 与表单库配合(react-hook-form)
const FormWithHookForm = () => {
const { register } = useForm();
const id = useId();
return (
<form>
<input {...register('name')} id={`${id}-name`} />
<label htmlFor={`${id}-name`}>Name</label>
</form>
);
};
2.1.5 useTransition 与 useDeferredValue
【React 18 并发特性】处理非紧急更新
// ✅ useTransition:标记非紧急更新
const SearchPage = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value); // 紧急:立即更新输入框
// startTransition 标记的更新是非紧急的
startTransition(() => {
// 这个更新可以被高优先级任务打断
setResults(expensiveSearch(value));
});
};
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <Results results={results} />}
</div>
);
};
// ✅ useDeferredValue:延迟更新(类似防抖但更智能)
const SearchResults = ({ query }) => {
// deferredQuery 会"落后"于 query
// 当 query 快速变化时,UI 仍能保持响应
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Results results={expensiveSearch(deferredQuery)} />
</div>
);
};
// ✅ 典型场景:大列表搜索
const BigList = ({ items, filter }) => {
const deferredFilter = useDeferredValue(filter);
// 即使 filter 快速变化,列表也能平滑渲染
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.includes(deferredFilter)
);
}, [items, deferredFilter]);
return (
<VirtualList items={filteredItems} />
);
};
2.2 自定义 Hooks 设计模式
2.2.1 自定义 Hook 核心原则
【复用逻辑】自定义 Hook 是 React 中逻辑复用的最佳方式
自定义 Hook 命名规则:
✅ use + 业务逻辑/功能名
useAuth, useCart, useDebounce, useFetch
❌ 不要用
withPermission (这是 HOC 的命名)
renderUserList (这是组件的命名)
🎯 基础自定义 Hook 模式
// 场景 1:封装数据获取逻辑
const useFetch = (url, options) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
// 防止组件卸载后 setState
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true; // 清理函数
};
}, [url]); // 依赖 url
return { data, loading, error, refetch: () => fetchData() };
};
// 使用
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <Skeleton />;
if (error) return <Error message={error} />;
return <Profile user={user} />;
};
🎯 进阶自定义 Hook:表单处理
// 封装复杂表单逻辑
const useForm = (initialValues, validate, onSubmit) => {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (field) => (e) => {
const { value } = e.target;
setValues(prev => ({ ...prev, [field]: value }));
// 实时校验
if (touched[field]) {
const error = validate({ [field]: value })[field];
setErrors(prev => ({ ...prev, [field]: error }));
}
};
const handleBlur = (field) => () => {
setTouched(prev => ({ ...prev, [field]: true }));
const error = validate(values)[field];
setErrors(prev => ({ ...prev, [field]: error }));
};
const handleSubmit = async (e) => {
e.preventDefault();
// 标记所有字段为已触摸
const allTouched = Object.keys(values).reduce(
(acc, key) => ({ ...acc, [key]: true }),
{}
);
setTouched(allTouched);
// 验证所有字段
const validationErrors = validate(values);
setErrors(validationErrors);
// 如果有错误,不提交
if (Object.values(validationErrors).some(Boolean)) {
return;
}
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset,
};
};
// 使用
const LoginForm = () => {
const {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
} = useForm(
{ email: '', password: '' },
validateForm,
async (data) => {
await api.login(data);
toast.success('登录成功!');
}
);
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange('email')}
onBlur={handleBlur('email')}
/>
{touched.email && errors.email && <span>{errors.email}</span>}
<input
name="password"
type="password"
value={values.password}
onChange={handleChange('password')}
onBlur={handleBlur('password')}
/>
{touched.password && errors.password && <span>{errors.password}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '登录中...' : '登录'}
</button>
</form>
);
};
2.2.2 常用工具 Hooks 集合
// useDebounce:防抖
const useDebounce = (value, delay = 300) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
// useLocalStorage:持久化状态
const useLocalStorage = (key, initialValue) => {
const [value, setValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = useCallback((newValue) => {
setValue(prev => {
const valueToStore = newValue instanceof Function
? newValue(prev)
: newValue;
try {
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('localStorage error:', error);
}
return valueToStore;
});
}, [key]);
return [value, setStoredValue];
};
// usePrevious:获取上一个渲染的值
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
// useClickOutside:检测点击外部
const useClickOutside = (ref, handler) => {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('click', listener);
return () => document.removeEventListener('click', listener);
}, [ref, handler]);
};
// useMediaQuery:响应式监听
const useMediaQuery = (query) => {
const [matches, setMatches] = useState(
() => window.matchMedia(query).matches
);
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (event) => setMatches(event.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
};
// 使用
const ResponsiveMenu = () => {
const isMobile = useMediaQuery('(max-width: 768px)');
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef();
useClickOutside(menuRef, () => setIsOpen(false));
return isMobile ? (
<MobileMenu ref={menuRef} isOpen={isOpen} />
) : (
<DesktopMenu />
);
};
2.3 组件复用模式
2.3.1 高阶组件(HOC)
【历史模式】曾是最流行的逻辑复用方式,现在推荐使用自定义 Hooks
// ❌ HOC 示例(了解即可,不再推荐)
// withLoading.ts
const withLoading = (Component) => {
return function WithLoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <Skeleton />;
}
return <Component {...props} />;
};
};
// 使用
const UserListWithLoading = withLoading(UserList);
// 问题:
// 1. 层层嵌套,调试困难(DevTools 显示为 XXX_withLoading_XXX_withAuth_...)
// 2. Props 命名可能冲突
// 3. 多个 HOC 组合时顺序可能影响结果
2.3.2 Render Props
【过渡方案】通过 props 传递渲染函数的模式
// ✅ Render Props 示例
const MouseTracker = ({ render }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove}>
{render(position)}
</div>
);
};
// 使用
const App = () => (
<MouseTracker
render={({ x, y }) => (
<div>
鼠标位置:{x}, {y}
</div>
)}
/>
);
// ✅ children 模式(更简洁)
const MouseTrackerChildren = ({ children }) => {
const [position, setPosition] = useState({ x: 0, y: 0 });
return (
<div onMouseMove={(e) => setPosition({ x: e.clientX, y: e.clientY })}>
{children(position)}
</div>
);
};
const App = () => (
<MouseTrackerChildren>
{({ x, y }) => <div>鼠标位置:{x}, {y}</div>}
</MouseTrackerChildren>
);
2.3.3 组合式组件(Compound Components)
【现代最佳实践】通过隐式状态共享实现灵活的组件组合
// ✅ 组合式组件:折叠面板
const AccordionContext = createContext(null);
const Accordion = ({ children, defaultOpen = null }) => {
const [openId, setOpenId] = useState(defaultOpen);
const toggle = useCallback((id) => {
setOpenId(prev => prev === id ? null : id);
}, []);
const value = useMemo(() => ({ openId, toggle }), [openId, toggle]);
return (
<AccordionContext.Provider value={value}>
<div className="accordion">{children}</div>
</AccordionContext.Provider>
);
};
Accordion.Item = ({ id, children }) => {
const { openId, toggle } = useContext(AccordionContext);
const isOpen = openId === id;
return (
<div className="accordion-item">
<Accordion.Header
isOpen={isOpen}
onClick={() => toggle(id)}
>
{children[0]}
</Accordion.Header>
<Accordion.Content isOpen={isOpen}>
{children[1]}
</Accordion.Content>
</div>
);
};
Accordion.Header = ({ children, isOpen, onClick }) => (
<div className="accordion-header" onClick={onClick}>
{children}
<span>{isOpen ? '▲' : '▼'}</span>
</div>
);
Accordion.Content = ({ children, isOpen }) => (
<div className={`accordion-content ${isOpen ? 'open' : ''}`}>
{children}
</div>
);
// 使用
const App = () => (
<Accordion defaultOpen="section1">
<Accordion.Item id="section1">
{['第一部分标题', '第一部分内容...']}
</Accordion.Item>
<Accordion.Item id="section2">
{['第二部分标题', '第二部分内容...']}
</Accordion.Item>
</Accordion>
);
// ✅ 另一个例子:Tabs 组件
const Tabs = ({ children, defaultTab }) => {
const [activeTab, setActiveTab] = useState(defaultTab);
const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
return (
<TabsContext.Provider value={value}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
);
};
Tabs.List = ({ children }) => (
<div className="tabs-list" role="tablist">
{children}
</div>
);
Tabs.Tab = ({ id, children }) => {
const { activeTab, setActiveTab } = useContext(TabsContext);
return (
<button
role="tab"
aria-selected={activeTab === id}
onClick={() => setActiveTab(id)}
>
{children}
</button>
);
};
Tabs.Panel = ({ id, children }) => {
const { activeTab } = useContext(TabsContext);
if (activeTab !== id) return null;
return (
<div role="tabpanel">
{children}
</div>
);
};
// 使用
const App = () => (
<Tabs defaultTab="tab1">
<Tabs.List>
<Tabs.Tab id="tab1">标签一</Tabs.Tab>
<Tabs.Tab id="tab2">标签二</Tabs.Tab>
</Tabs.List>
<Tabs.Panel id="tab1">内容一</Tabs.Panel>
<Tabs.Panel id="tab2">内容二</Tabs.Panel>
</Tabs>
);
📊 组件复用模式对比
| 模式 | 灵活性 | 易用性 | 调试难度 | 推荐度 |
|---|---|---|---|---|
| HOC | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ❌ 不推荐 |
| Render Props | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 组合式组件 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ 强烈推荐 |
| 自定义 Hooks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ 首选(逻辑复用) |
三、状态管理 & 数据层
3.1 现代化状态管理方案
3.1.1 状态管理选型矩阵
【架构决策】根据项目规模和复杂度选择合适的方案
| 方案 | 适用场景 | 学习曲线 | Bundle Size | 状态规范化 | 推荐度 |
|---|---|---|---|---|---|
| useState + useReducer | 局部/模块状态 | 低 | 0KB | ❌ | ⭐⭐⭐⭐⭐ |
| Context API | 全局共享状态 | 低 | 0kb | ❌ | ⭐⭐⭐⭐ |
| Zustand | 中大型应用 | 中 | ~3kb | ✅ | ⭐⭐⭐⭐⭐ |
| Jotai | 原子化状态 | 中 | ~2kb | ✅ | ⭐⭐⭐⭐ |
| Redux Toolkit | 大型企业应用 | 高 | ~10kb | ✅✅ | ⭐⭐⭐⭐ |
| Recoil | 实验性/特殊场景 | 中 | ~30kb | ✅ | ⭐⭐⭐ |
3.1.2 Zustand —— 极简但强大
【推荐首选】API 简洁,TypeScript 支持好,性能优秀
// store/counter.ts
import { create } from 'zustand';
import { devtools, subscribeWithSelector } from 'zustand/middleware';
interface CounterState {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterState>()(
devtools(
subscribeWithSelector((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
})),
{ name: 'CounterStore' }
)
);
// 高级用法:split slices
// store/userStore.ts
import { create } from 'zustand';
interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
setUser: (user: User | null) => void;
fetchUser: (id: string) => Promise<void>;
}
export const useUserStore = create<UserState>((set, get) => ({
user: null,
isLoading: false,
error: null,
setUser: (user) => set({ user }),
fetchUser: async (id) => {
set({ isLoading: true, error: null });
try {
const response = await api.getUser(id);
set({ user: response.data, isLoading: false });
} catch (error) {
set({ error: error.message, isLoading: false });
}
},
}));
// 在组件中使用
const Counter = () => {
const { count, increment } = useCounterStore();
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
};
// Selector 优化:避免不必要的重渲染
const ExpensiveComponent = () => {
// ❌ 每次 count 变化都会重渲染
// const { count, increment } = useCounterStore();
// ✅ 只有 count 变化时才重渲染
const count = useCounterStore((state) => state.count);
const increment = useCounterStore((state) => state.increment);
return <div>{/* ... */}</div>;
};
// 订阅特定变化
useCounterStore.subscribe(
(state) => state.count,
(count, previousCount) => {
console.log(`Count changed: ${previousCount} → ${count}`);
}
);
3.1.3 Jotai —— 原子化状态管理
【原子化思想】每个状态都是独立的原子,组件按需订阅
// atoms/index.ts
import { atom } from 'jotai';
// 基础原子
const countAtom = atom(0);
const userAtom = atom<User | null>(null);
// 派生原子(类似 useMemo)
const doubledCountAtom = atom((get) => get(countAtom) * 2);
// 带 write 的原子
const incrementAtom = atom(
(get) => get(countAtom),
(get, set) => {
set(countAtom, get(countAtom) + 1);
}
);
// 异步原子
const userDataAtom = atom(async (get) => {
const userId = get(userIdAtom);
if (!userId) return null;
return fetchUser(userId);
});
// 在组件中使用
import { useAtom } from 'jotai';
const Counter = () => {
// 订阅单个原子
const [count] = useAtom(countAtom);
const [, increment] = useAtom(incrementAtom);
return (
<div>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
};
// 派生状态
const Display = () => {
const [doubled] = useAtom(doubledCountAtom);
return <div>Doubled: {doubled}</div>;
};
// Jotai + React Query 配合
import { useAtom } from 'jotai';
import { atomWithQuery } from 'jotai/query';
const userQueryAtom = atomWithQuery((get) => ({
queryKey: ['user', get(userIdAtom)],
queryFn: async ({ queryKey }) => {
const [, userId] = queryKey;
return fetchUser(userId);
},
}));
const UserProfile = () => {
const [{ data, isLoading }] = useAtom(userQueryAtom);
// ...
};
3.1.4 Redux Toolkit(大型项目标准)
【企业级方案】适合大型团队、复杂状态、需要 DevTools 调试的场景
// store/slices/userSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
interface UserState {
user: User | null;
isLoading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
isLoading: false,
error: null,
};
export const fetchUserById = createAsyncThunk(
'user/fetchById',
async (userId: string, { rejectWithValue }) => {
try {
const response = await api.getUser(userId);
return response.data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action: PayloadAction<User | null>) => {
state.user = action.payload;
},
updateUserName: (state, action: PayloadAction<string>) => {
if (state.user) {
state.user.name = action.payload;
}
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(fetchUserById.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchUserById.fulfilled, (state, action) => {
state.isLoading = false;
state.user = action.payload;
})
.addCase(fetchUserById.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
},
});
export const { setUser, updateUserName, clearError } = userSlice.actions;
export default userSlice.reducer;
// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import userReducer from './slices/userSlice';
import cartReducer from './slices/cartSlice';
export const store = configureStore({
reducer: {
user: userReducer,
cart: cartReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['user/fetchById/fulfilled'],
ignoredPaths: ['user.user.metadata'],
},
}),
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// Typed hooks
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// 组件中使用
const UserProfile = () => {
const { user, isLoading, error } = useAppSelector((state) => state.user);
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(fetchUserById('123'));
}, [dispatch]);
return (
<div>
{isLoading && <Skeleton />}
{error && <Error message={error} />}
{user && <Profile user={user} />}
</div>
);
};
3.2 数据请求与缓存
3.2.1 React Query / TanStack Query
【数据获取金标准】替代 Redux 处理服务器状态,是现代 React 应用的事实标准
// ✅ 基础用法
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
const UserProfile = ({ userId }) => {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.getUser(userId),
staleTime: 5 * 60 * 1000, // 5分钟内不重新获取
cacheTime: 10 * 60 * 1000, // 缓存保留10分钟
enabled: !!userId, // 条件执行
});
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <Profile user={data} />;
};
// ✅ 创建 mutation(增删改)
const CreateUser = () => {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newUser) => api.createUser(newUser),
onSuccess: (data) => {
// 使 user 列表缓存失效,重新获取
queryClient.invalidateQueries({ queryKey: ['users'] });
// 或者直接更新缓存
queryClient.setQueryData(['user', data.id], data);
},
onError: (error) => {
toast.error(error.message);
},
});
return (
<button
onClick={() => mutation.mutate({ name: 'John' })}
disabled={mutation.isLoading}
>
{mutation.isLoading ? '创建中...' : '创建用户'}
</button>
);
};
// ✅ 高级配置
const useUsers = (params) => {
return useQuery({
queryKey: ['users', params],
queryFn: () => api.getUsers(params),
// 分页支持
keepPreviousData: true, // 保持上一页数据直到新数据返回
// 重试配置
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
// 轮询
refetchInterval: 30000, // 每30秒轮询(仅在窗口聚焦时)
refetchIntervalInBackground: false,
});
};
// ✅ 并行查询
const Dashboard = ({ userId }) => {
const userQuery = useQuery({ queryKey: ['user', userId], queryFn: () => api.getUser(userId) });
const ordersQuery = useQuery({ queryKey: ['orders', userId], queryFn: () => api.getOrders(userId) });
const notificationsQuery = useQuery({ queryKey: ['notifications'], queryFn: () => api.getNotifications() });
// 或者使用 useQueries
const results = useQueries({
queries: [
{ queryKey: ['user', userId], queryFn: () => api.getUser(userId) },
{ queryKey: ['orders', userId], queryFn: () => api.getOrders(userId) },
],
});
};
3.2.2 SWR —— Vercel 出品的轻量替代
// ✅ SWR 基础用法
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then(res => res.json());
const UserProfile = ({ userId }) => {
const { data, error, isLoading, mutate } = useSWR(
userId ? `/api/user/${userId}` : null,
fetcher,
{
revalidateOnFocus: true, // 窗口聚焦时重新验证
revalidateOnReconnect: true,
dedupingInterval: 5000, // 5秒内相同请求去重
}
);
const handleUpdate = async () => {
await mutate(
api.updateUser(userId, newData), // 更新数据
{ revalidate: true } // 重新验证
);
};
return (
<div>
{isLoading && <Skeleton />}
{error && <Error />}
{data && <Profile user={data} />}
</div>
);
};
// ✅ 乐观更新
const UpdateName = () => {
const { data, mutate } = useSWR('/api/user', fetcher);
const handleSubmit = async (name) => {
// 乐观更新:立即显示新名字
const previousData = data;
mutate({ ...data, name }, false);
try {
await api.updateName(name);
} catch {
// 失败时回滚
mutate(previousData, false);
}
};
return <Form onSubmit={handleSubmit} initialName={data?.name} />;
};
📊 React Query vs SWR 对比
| 特性 | React Query | SWR |
|---|---|---|
| Bundle Size | ~13kb | ~4kb |
| 社区活跃度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| DevTools | ✅ 官方提供 | ❌ 第三方 |
| 缓存管理 | 强大且灵活 | 基础但够用 |
| 适用场景 | 中大型应用 | 小中型应用 |
| TypeScript 支持 | ✅ 优秀 | ✅ 良好 |
| 学习曲线 | 略陡 | 平缓 |
3.3 状态划分策略
3.3.1 三层状态架构
【架构原则】根据状态的使用范围和更新频率选择合适的管理方案
┌─────────────────────────────────────────────────────────────┐
│ │
│ 【服务器状态】Server State │
│ ├── 来源:API 请求、GraphQL │
│ ├── 管理:React Query / SWR / Apollo Client │
│ ├── 特点:异步、缓存、自动同步、需要考虑数据一致性 │
│ └── 存储位置:Query Cache │
│ │
│ 【全局状态】Global State │
│ ├── 来源:用户信息、主题、语言设置、购物车 │
│ ├── 管理:Zustand / Context + useReducer / Redux Toolkit │
│ ├── 特点:同步、需要跨组件共享 │
│ └── 存储位置:内存 / LocalStorage(持久化) │
│ │
│ 【本地状态】Local State │
│ ├── 来源:表单输入、UI 状态(弹窗、展开/折叠) │
│ ├── 管理:useState / useReducer │
│ ├── 特点:同步、不需要跨组件共享 │
│ └── 存储位置:组件内部 │
│ │
└─────────────────────────────────────────────────────────────┘
3.3.2 状态管理方案决策树
需要管理状态时,按照以下顺序决策:
1. 【是服务器状态吗?】(用户数据、列表等来自 API)
│
├─ YES → 使用 React Query / SWR
│ ├─ 需要乐观更新?→ React Query
│ ├─ 需要分页/无限滚动?→ React Query
│ └─ 简单场景?→ SWR
│
└─ NO → 继续判断...
2. 【需要跨组件共享吗?】
│
├─ YES → 是全局共享还是模块共享?
│ ├─ 全局(主题/用户/语言)→ Context API / Zustand
│ └─ 模块(单个功能模块)→ Zustand / useReducer
│
└─ NO → 使用 useState
3. 【状态逻辑复杂吗?】(多个相关状态、多种操作)
│
├─ YES → useReducer / Zustand
│
└─ NO → useState
3.3.3 实践案例:电商应用状态架构
// 【服务器状态】产品数据 - React Query
export const useProducts = (categoryId) => useQuery({
queryKey: ['products', categoryId],
queryFn: () => api.getProducts(categoryId),
});
// 【全局状态】用户信息 - Context
const UserContext = createContext(null);
// 【全局状态】购物车 - Zustand
const useCartStore = create(persist(
(set, get) => ({
items: [],
addItem: (product) => set(state => ({
items: [...state.items, { ...product, quantity: 1 }]
})),
removeItem: (id) => set(state => ({
items: state.items.filter(item => item.id !== id)
})),
total: () => get().items.reduce((sum, item) => sum + item.price * item.quantity, 0),
}),
{ name: 'cart-storage' } // 持久化到 localStorage
));
// 【模块状态】订单表单 - useReducer
const orderReducer = (state, action) => {
switch (action.type) {
case 'SET_ADDRESS':
return { ...state, shippingAddress: action.payload };
case 'SET_PAYMENT':
return { ...state, paymentMethod: action.payload };
case 'APPLY_COUPON':
return { ...state, coupon: action.payload, discount: calculateDiscount(action.payload) };
case 'RESET':
return initialOrderState;
default:
return state;
}
};
// 【本地状态】UI 状态 - useState
const CheckoutPage = () => {
// 服务器状态
const { data: cartItems } = useProducts('cart');
// 全局状态
const { items, removeItem } = useCartStore();
// 模块状态
const [order, dispatch] = useReducer(orderReducer, initialOrderState);
// 本地状态
const [isModalOpen, setIsModalOpen] = useState(false);
// ...
};
四、路由 & 权限进阶
4.1 路由进阶与代码分割
4.1.1 路由懒加载与代码分割
【性能优化】减少首屏加载时间,按需加载路由组件
// ✅ React.lazy + Suspense 实现路由懒加载
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
// 懒加载路由组件
const HomePage = lazy(() => import('./pages/Home'));
const AboutPage = lazy(() => import('./pages/About'));
const ProductPage = lazy(() => import('./pages/Product'));
const DashboardPage = lazy(() => import('./pages/Dashboard'));
const NotFoundPage = lazy(() => import('./pages/NotFound'));
// 路由配置
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <HomePage />,
},
{
path: 'about',
element: <AboutPage />,
},
{
path: 'products/:productId',
element: <ProductPage />,
},
],
},
{
path: '/dashboard',
element: <PrivateRoute />,
children: [
{
path: '',
element: <DashboardPage />,
},
],
},
{
path: '*',
element: <NotFoundPage />,
},
]);
// App 入口
const App = () => (
// Suspense 处理加载状态
<Suspense fallback={<PageLoader />}>
<RouterProvider router={router} />
</Suspense>
);
// ✅ 使用 Vite 的预加载功能优化体验
const ProductPage = lazy(() =>
import('./pages/Product').then(module => {
// Vite 会预加载这个 chunk
return module;
})
);
// ✅ 基于路由的数据加载(loader)
import { createBrowserRouter, useLoaderData } from 'react-router-dom';
const ProductDetail = () => {
const { product } = useLoaderData(); // 从 loader 获取数据
return <ProductView product={product} />;
};
const productLoader = async ({ params }) => {
const product = await api.getProduct(params.productId);
return { product };
};
const router = createBrowserRouter([
{
path: 'products/:productId',
element: <ProductDetail />,
loader: productLoader, // 路由级别数据预加载
},
]);
4.1.2 动态路由与嵌套路由
// ✅ 动态路由参数
const router = createBrowserRouter([
{
path: '/users/:userId', // 动态段
element: <UserLayout />,
children: [
{
path: '', // /users/:userId
element: <UserProfile />,
},
{
path: 'posts', // /users/:userId/posts
element: <UserPosts />,
},
{
path: 'settings/*', // 嵌套通配符
element: <UserSettings />,
},
],
},
]);
// ✅ useParams 获取参数
const UserProfile = () => {
const { userId } = useParams(); // { userId: '123' }
return <div>User ID: {userId}</div>;
};
// ✅ 搜索参数
const SearchPage = () => {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') || '';
const page = parseInt(searchParams.get('page') || '1');
const handleSearch = (newQuery) => {
setSearchParams({ q: newQuery, page: '1' });
};
const handlePageChange = (newPage) => {
setSearchParams({ q: query, page: String(newPage) });
};
return (
<div>
<SearchInput value={query} onChange={handleSearch} />
<Results query={query} page={page} />
<Pagination page={page} onChange={handlePageChange} />
</div>
);
};
// ✅ 编程式导航
const NavigateComponent = () => {
const navigate = useNavigate();
const goToProduct = (id) => {
navigate(`/products/${id}`);
};
const goBack = () => {
navigate(-1); // 浏览器历史的后退
};
const replaceToHome = () => {
navigate('/', { replace: true }); // 替换当前历史记录
};
return (
<div>
<button onClick={() => goToProduct('123')}>查看产品</button>
<button onClick={goBack}>返回</button>
<button onClick={replaceToHome}>回首页(替换)</button>
</div>
);
};
4.1.3 路由守卫与权限控制
【安全关键】路由守卫必须配合后端权限验证,前端只是 UX 优化
// ✅ 路由守卫组件
const ProtectedRoute = ({ children, requiredRoles = [] }) => {
const { isAuthenticated, user, isLoading } = useAuth();
const location = useLocation();
// 加载中状态
if (isLoading) {
return <FullPageSpinner />;
}
// 未登录:重定向到登录页
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// 角色验证
if (requiredRoles.length > 0 && !requiredRoles.includes(user.role)) {
return <Navigate to="/unauthorized" replace />;
}
return children;
};
// ✅ 带权限的路由配置
const router = createBrowserRouter([
{
path: '/admin',
element: (
<ProtectedRoute requiredRoles={['admin', 'super_admin']}>
<AdminLayout />
</ProtectedRoute>
),
children: [
{ path: 'users', element: <UserManagement /> },
{ path: 'settings', element: <AdminSettings /> },
],
},
{
path: '/profile',
element: (
<ProtectedRoute>
<ProfilePage />
</ProtectedRoute>
),
},
]);
// ✅ 路由级别的数据加载(权限检查)
const adminLoader = async () => {
const user = await getCurrentUser();
if (!user || !hasPermission(user, 'admin:access')) {
throw new Response('Unauthorized', { status: 401 });
}
return { adminData: await fetchAdminData() };
};
const router = createBrowserRouter([
{
path: '/admin',
element: <AdminLayout />,
loader: adminLoader, // 路由加载时检查权限
},
]);
4.2 错误处理与边界
4.2.1 React 错误边界(Error Boundaries)
【错误恢复】错误边界是 React 16 引入的用于捕获子组件错误的机制
// ✅ 类组件实现错误边界
class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error caught:', error, errorInfo);
// 上报错误到监控服务
logErrorToSentry(error, errorInfo);
}
resetError = () => {
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback;
}
return (
<div className="error-boundary">
<h2>出现了一些问题</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.resetError}>重试</button>
<a href="/">返回首页</a>
</div>
);
}
return this.props.children;
}
}
// ✅ 函数组件实现(使用 react-error-boundary 库更方便)
import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
const fallbackRender = ({ error, resetErrorBoundary }) => (
<div role="alert">
<p>出现错误了:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>重试</button>
</div>
);
const App = () => (
<ReactErrorBoundary fallbackRender={fallbackRender}>
<MyComponent />
</ReactErrorBoundary>
);
4.2.2 路由级错误处理
// ✅ 路由配置中的 errorElement
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <GlobalErrorPage />, // 任何子路由错误都会显示
children: [
{
path: 'products/:id',
element: <ProductPage />,
errorElement: <ProductErrorPage />, // 产品页专属错误
},
],
},
]);
// ✅ useRouteError 获取错误信息
import { useRouteError } from 'react-router-dom';
const GlobalErrorPage = () => {
const error = useRouteError();
return (
<div className="error-page">
<h1>{error.status || 'Oops!'}</h1>
<p>{error.statusText || error.message}</p>
<a href="/">返回首页</a>
</div>
);
};
⚠️ 错误边界无法捕获的情况
// ❌ 以下情况错误边界无法捕获:
// 1. 事件处理函数中的错误
const BadComponent = () => (
<button onClick={() => { throw new Error('Oops!'); }}>
Click me
</button>
);
// 2. 异步代码中的错误(setTimeout, Promise)
const BadAsyncComponent = () => {
useEffect(() => {
setTimeout(() => {
throw new Error('Oops!'); // 不会被捕获
}, 1000);
}, []);
return <div>Async Error</div>;
};
// 3. 服务端渲染(SSR)错误
// 4. 错误边界组件本身的错误
五、TypeScript + React 工程化
5.1 TypeScript 核心概念
5.1.1 类型基础
// ✅ 基本类型
const name: string = 'John';
const age: number = 30;
const isActive: boolean = true;
const hobbies: string[] = ['reading', 'coding'];
const scores: Array<number> = [90, 85, 88];
// ✅ 对象类型
interface User {
id: string;
name: string;
email: string;
age?: number; // 可选属性
readonly createdAt: Date; // 只读属性
role: 'admin' | 'user' | 'guest'; // 联合类型
}
// ✅ 函数类型
type Handler = (event: MouseEvent) => void;
type AsyncHandler = (data: string) => Promise<void>;
// ✅ 泛型
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface ListResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
}
// 使用泛型
const response: ApiResponse<User> = {
data: { id: '1', name: 'John', email: 'john@example.com', createdAt: new Date(), role: 'user' },
status: 200,
message: 'Success',
};
// ✅ 泛型约束
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
5.1.2 接口 vs 类型别名
// ✅ interface:适合声明对象结构,支持声明合并
interface User {
id: string;
name: string;
}
interface User {
email: string; // 合并到上面的 User
}
// 继承
interface Admin extends User {
permissions: string[];
}
// ✅ type alias:适合复杂类型联合、交叉
type Status = 'pending' | 'loading' | 'success' | 'error';
type Result<T, E = Error> = { data: T; error: null } | { data: null; error: E };
// 交叉类型
type ExtendedUser = User & { extra: string };
// ✅ 最佳实践:
// - 用 interface 声明对象结构(可扩展性好)
// - 用 type alias 处理联合类型、元组、工具类型
5.1.3 实用工具类型
// ✅ 常用工具类型
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Partial:所有属性变为可选
type PartialUser = Partial<User>;
// { id?: string; name?: string; email?: string; age?: number }
// Required:所有属性变为必需
type RequiredUser = Required<User>;
// Pick:选取部分属性
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }
// Omit:排除部分属性
type UserWithoutEmail = Omit<User, 'email'>;
// { id: string; name: string; age: number }
// Record:键值对
type UserMap = Record<string, User>;
// ReturnType:提取函数返回值
const getUser = () => ({ id: '1', name: 'John' });
type UserResult = ReturnType<typeof getUser>;
// Parameters:提取函数参数
function createUser(name: string, age: number) { /* ... */ }
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
5.2 React + TypeScript 最佳实践
5.2.1 组件 Props 类型定义
// ✅ 函数组件 Props 类型
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
}
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
type = 'button',
className,
}) => {
return (
<button
type={type}
className={`btn btn--${variant} btn--${size} ${className || ''}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
// ✅ 推荐写法:使用 Props 类型参数(React 18+)
const Button = <T extends React.ReactNode>({
children,
variant = 'primary',
}: {
children: T;
variant?: 'primary' | 'secondary';
}) => {
return <button className={`btn btn--${variant}`}>{children}</button>;
};
// ✅ 使用 as Props 的组件
interface FormProps {
children: React.ReactNode;
onSubmit: (data: FormData) => void;
}
const Form: React.FC<FormProps> = ({ children, onSubmit }) => {
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(Object.fromEntries(formData));
};
return <form onSubmit={handleSubmit}>{children}</form>;
};
Form.Input = ({ name, ...props }) => (
<input name={name} {...props} />
);
5.2.2 Hooks 类型定义
// ✅ useState 类型推断
const [count, setCount] = useState(0); // number
const [user, setUser] = useState<User | null>(null); // 显式指定
const [loading, setLoading] = useState(false); // boolean
const [items, setItems] = useState<Item[]>([]); // 数组
// ✅ useRef 类型
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// 使用
inputRef.current?.focus();
inputRef.current?.value;
// ✅ useEffect 返回类型
useEffect(() => {
const controller = new AbortController();
return () => controller.abort(); // cleanup 函数类型自动推断
}, []);
// ✅ useReducer 泛型
interface State {
count: number;
loading: boolean;
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number }
| { type: 'setLoading'; payload: boolean };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'set':
return { ...state, count: action.payload };
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, { count: 0, loading: false });
// ✅ 自定义 Hooks 类型
function useFetch<T>(url: string): {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
} {
// ...
}
5.2.3 事件处理器类型
// ✅ 常用事件类型
const EventExamples = () => {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.name);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Submitted');
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
console.log('Focused:', e.target.name);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
console.log('Enter pressed');
}
};
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
console.log('ScrollTop:', e.currentTarget.scrollTop);
};
return (
<form onSubmit={handleSubmit}>
<input
name="username"
onChange={handleChange}
onFocus={handleFocus}
onKeyDown={handleKeyDown}
/>
<button name="submit" onClick={handleClick}>Submit</button>
</form>
);
};
// ✅ 拖拽事件
const Draggable = () => {
const handleDragStart = (e: React.DragEvent<HTMLDivElement>) => {
e.dataTransfer.setData('text/plain', 'dragged-item');
};
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
const data = e.dataTransfer.getData('text/plain');
console.log('Dropped:', data);
};
return (
<div
draggable
onDragStart={handleDragStart}
onDrop={handleDrop}
>
Drag me
</div>
);
};
5.3 类型工程化
5.3.1 全局类型声明
// ✅ types/global.d.ts
declare global {
interface Window {
__INITIAL_DATA__: any;
gtag: (...args: any[]) => void;
}
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
REACT_APP_API_URL: string;
REACT_APP_VERSION: string;
}
}
}
// ✅ 模块声明
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.svg' {
const content: React.FC<React.SVGProps<SVGElement>>;
export default content;
}
declare module '*.mp4' {
const src: string;
export default src;
}
// ✅ 全局工具类型
type Nullable<T> = T | null;
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : any;
// ✅ API 响应类型
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
5.3.2 类型守卫与类型断言
// ✅ 类型守卫
interface Cat {
meow(): void;
}
interface Dog {
bark(): void;
}
function isCat(animal: Cat | Dog): animal is Cat {
return (animal as Cat).meow !== undefined;
}
function speak(animal: Cat | Dog) {
if (isCat(animal)) {
animal.meow();
} else {
animal.bark();
}
}
// ✅ in 操作符类型守卫
function isClickable(element: HTMLElement): element is HTMLElement {
return 'onclick' in element || element.getAttribute('role') === 'button';
}
// ✅ typeof 类型守卫
type StringOrNumber = string | number;
function isString(value: StringOrNumber): value is string {
return typeof value === 'string';
}
// ✅ 类型断言
// as 断言(推荐)
const input = document.getElementById('myInput') as HTMLInputElement;
input.value = 'hello';
// 非空断言(慎用)
const name = user!.name; // 假设 user 一定存在
// 类型映射
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
5.3.3 类型复用与抽象
// ✅ 创建可复用的类型工具
// types/api.ts
export const createApiSlice = <T>(endpoint: string) => ({
queries: {
list: () => useQuery<T[]>(`${endpoint}`),
byId: (id: string) => useQuery<T>(`${endpoint}/${id}`),
},
mutations: {
create: () => useMutation<T, T>(`${endpoint}`),
update: () => useMutation<T, { id: string; data: Partial<T> }>(`${endpoint}`),
delete: () => useMutation<void, string>(`${endpoint}`),
},
});
// 使用
const userApi = createApiSlice<User>('/api/users');
const { data: users } = userApi.queries.list();
const { data: user } = userApi.queries.byId('123');
六、性能优化
6.1 渲染优化策略
6.1.1 React.memo 最佳实践
【避免重渲染】React.memo 包裹组件,防止 props 未变化时的重渲染
// ✅ 基础用法
const MemoizedComponent = React.memo(function MyComponent({ name, onClick }) {
console.log('Component rendered'); // 观察渲染次数
return (
<div onClick={onClick}>
{name}
</div>
);
});
// ✅ 自定义比较函数(性能关键)
const OptimizedListItem = React.memo(
({ id, name, price }) => (
<li key={id}>
{name} - ${price}
</li>
),
// 自定义比较:仅在 id 和 price 变化时重渲染
(prevProps, nextProps) => {
return (
prevProps.id === nextProps.id &&
prevProps.price === nextProps.price
);
}
);
// ❌ 常见错误:比较函数返回 false(永远重渲染)
const BadMemo = React.memo(
({ data }) => <div>{data.map(item => item.name)}</div>,
() => false // 💀 总是返回 false = 永远重渲染
);
// ✅ 正确的对象比较
const GoodParent = () => {
const [count, setCount] = useState(0);
// ❌ 每次渲染都创建新对象
const config = { padding: 20 };
// ✅ 使用 useMemo 稳定对象引用
const config = useMemo(() => ({ padding: 20 }), []);
// ✅ 或者传递原始值
const padding = 20;
return (
<MemoizedChild
config={config}
padding={padding}
onClick={() => setCount(c => c + 1)}
/>
);
};
const MemoizedChild = React.memo(({ config, padding, onClick }) => {
console.log('MemoizedChild rendered');
return (
<div style={{ padding }} onClick={onClick}>
<DeepTree data={config} />
</div>
);
});
6.1.2 常见重渲染原因与解决方案
// ✅ 场景 1:内联函数作为 props
const BadParent = () => {
const [count, setCount] = useState(0);
return (
<>
<Child onClick={() => console.log('clicked')} /> {/* 新函数每次 */}
<button onClick={() => setCount(c => c + 1)}>Re-render</button>
</>
);
};
const GoodParent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(c => c + 1)}>Re-render</button>
</>
);
};
// ✅ 场景 2:内联对象作为 props
const BadParent2 = () => {
return <Child style={{ color: 'red' }} />; // 新对象每次
};
const GoodParent2 = () => {
const style = useMemo(() => ({ color: 'red' }), []);
return <Child style={style} />;
};
// ✅ 场景 3:数组作为 props
const BadParent3 = () => {
return <List data={[1, 2, 3]} />; // 新数组每次
};
const GoodParent3 = () => {
const data = useMemo(() => [1, 2, 3], []);
return <List data={data} />;
};
6.1.3 状态位置优化
【状态提升的艺术】把状态放在需要它的地方,而非更高层级
// ❌ 过度提升:导致不必要的重渲染
const过度提升 = () => {
const [count, setCount] = useState(0);
return (
<div>
<Header /> {/* 不需要 count 但会重渲染 */}
<Sidebar /> {/* 不需要 count 但会重渲染 */}
<Main>
<Counter />
</Main>
</div>
);
};
// ✅ 优化:状态放在需要它的组件内部
const 优化后 = () => (
<div>
<Header />
<Sidebar />
<Main>
<Counter /> {/* count 状态在这里 */}
</Main>
</div>
);
// ✅ 共享状态但独立订阅
const SharedCounter = ({ initialCount = 0 }) => {
const [count, setCount] = useState(0);
return <CounterContext.Provider value={{ count, setCount }}>
{children}
</CounterContext.Provider>;
};
const CounterDisplay = () => {
const { count } = useContext(CounterContext);
return <div>{count}</div>; // 仅 count 变化时重渲染
};
const CounterButton = () => {
const { setCount } = useContext(CounterContext);
return <button onClick={() => setCount(c => c + 1)}>+</button>;
};
6.2 资源优化
6.2.1 图片优化
// ✅ 响应式图片
const ResponsiveImage = ({ src, alt }) => (
<picture>
<source
media="(min-width: 1024px)"
srcSet={`${src}-1024.jpg`}
/>
<source
media="(min-width: 768px)"
srcSet={`${src}-768.jpg`}
/>
<img
src={`${src}-480.jpg`}
alt={alt}
loading="lazy" // 懒加载
decoding="async" // 异步解码
/>
</picture>
);
// ✅ 使用 modern 图片格式
const ModernImage = ({ src, alt }) => (
<picture>
<source srcSet={`${src}.avif`} type="image/avif" />
<source srcSet={`${src}.webp`} type="image/webp" />
<img src={`${src}.jpg`} alt={alt} />
</picture>
);
// ✅ 占位符与骨架屏
const ImageWithSkeleton = ({ src, alt }) => {
const [loaded, setLoaded] = useState(false);
return (
<div className={loaded ? '' : 'skeleton'}>
{!loaded && <div className="skeleton-placeholder" />}
<img
src={src}
alt={alt}
onLoad={() => setLoaded(true)}
style={{ opacity: loaded ? 1 : 0 }}
/>
</div>
);
};
6.2.2 虚拟列表(长列表优化)
【性能关键】对于 100+ 条数据的列表,必须使用虚拟滚动
// ✅ react-window 实现虚拟列表
import { FixedSizeList as List } from 'react-window';
import { VariableSizeList } from 'react-window';
const VirtualList = ({ items }) => {
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
)}
</List>
);
};
// ✅ 可变高度列表(需要测量)
const VariableHeightList = ({ items }) => {
const getItemSize = (index) => {
// 根据内容计算高度
return items[index].hasImage ? 100 : 50;
};
return (
<VariableSizeList
height={400}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{({ index, style }) => (
<div style={style}>
<VariableHeightItem item={items[index]} />
</div>
)}
</VariableSizeList>
);
};
// ✅ react-virtuoso(更现代,支持分组)
import { Virtuoso } from 'react-virtuoso';
const AutoSizingList = ({ items }) => (
<Virtuoso
style={{ height: 400 }}
totalCount={items.length}
itemContent={(index) => <Item data={items[index]} />}
/>
);
6.2.3 代码分割策略
// ✅ 路由级别分割
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// ✅ 组件级别分割(按需加载)
const ChartComponent = lazy(() => import('./components/Chart'));
const HeavyEditor = lazy(() => import('./components/HeavyEditor'));
// ✅ 条件分割(仅在需要时加载)
const ConditionalFeature = ({ showFeature }) => {
const Feature = useMemo(() =>
showFeature ? lazy(() => import('./Feature')) : null,
[showFeature]
);
return Feature ? (
<Suspense fallback={<Spinner />}>
<Feature />
</Suspense>
) : null;
};
// ✅ Preload(提前加载)
const ProductPage = lazy(() => import('./pages/Product'));
// 在用户可能访问之前预加载
const preloadProductPage = () => {
ProductPage(); // 开始加载,但不阻塞当前渲染
};
// hover 时预加载
const ProductLink = ({ id }) => (
<Link
to={`/products/${id}`}
onMouseEnter={() => {
import('./pages/Product'); // 预加载
}}
>
View Product
</Link>
);
6.3 构建优化
6.3.1 Tree Shaking 与代码压缩
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
target: 'es2015', // 兼容到 ES2015
minify: 'terser', // 使用 terser 压缩
terserOptions: {
compress: {
drop_console: true, // 生产环境移除 console
drop_debugger: true,
},
},
rollupOptions: {
output: {
// 手动分包
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
ui: ['antd', '@ant-design/icons'],
},
},
},
// 启用 gzip 压缩
reportCompressedSize: true,
},
});
6.3.2 依赖分析
# 分析 bundle 组成
npm install rollup-plugin-visualizer -D
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
plugins: [
react(),
visualizer({
filename: 'bundle-stats.html', // 生成可视化报告
open: true,
}),
];
# 运行构建后打开 bundle-stats.html 查看组成
npm run build && open bundle-stats.html
七、样式 & 工程化进阶
7.1 样式方案深度对比
7.1.1 CSS Modules vs Styled Components vs Tailwind CSS
【选型决策】根据项目特点和团队选择合适的样式方案
| 维度 | CSS Modules | Styled Components | Tailwind CSS |
|---|---|---|---|
| Bundle Size | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 运行时开销 | 无 | CSS-in-JS 运行时 | 无 |
| 学习曲线 | 低 | 中 | 中(大量工具类) |
| IDE 支持 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 样式复用 | 手动 import | 继承/组合 | @apply 指令 |
| 动态样式 | ❌ 需配合 CSS 变量 | ✅ 原生支持 | ⚠️ 通过 class 切换 |
| 适用场景 | 中大型项目 | 需要主题切换 | 快速原型/小团队 |
7.1.2 CSS Modules 实战
/* Button.module.css */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.button--primary {
background: var(--color-primary);
color: white;
}
.button--secondary {
background: transparent;
border: 1px solid var(--color-border);
}
.button--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon {
margin-right: 8px;
}
// Button.tsx
import styles from './Button.module.css';
interface ButtonProps {
variant?: 'primary' | 'secondary';
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
disabled = false,
children,
onClick,
}) => {
return (
<button
className={`${styles.button} ${styles[`button--${variant}`]} ${disabled ? styles['button--disabled'] : ''}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
7.1.3 Tailwind CSS 实战
// ✅ Tailwind 配置
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#1890ff',
secondary: '#52c41a',
},
spacing: {
18: '4.5rem',
},
},
},
plugins: [],
};
// ✅ 使用
const TailwindButton = ({ children, variant = 'primary', onClick }) => (
<button
onClick={onClick}
className={`
px-4 py-2 rounded-md font-medium transition-colors
${variant === 'primary'
? 'bg-blue-500 text-white hover:bg-blue-600'
: 'bg-gray-200 text-gray-800 hover:bg-gray-300'}
`}
>
{children}
</button>
);
// ✅ @apply 复用样式
.btn-base {
@apply px-4 py-2 rounded-md font-medium transition-colors;
}
.btn-primary {
@apply btn-base bg-blue-500 text-white hover:bg-blue-600;
}
7.2 测试基础
7.2.1 Jest + React Testing Library 基础
【质量保障】测试不是为了覆盖率,而是为了信心
// ✅ 组件测试
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
describe('LoginForm', () => {
const onSubmit = vi.fn();
beforeEach(() => {
onSubmit.mockClear();
});
it('should render form fields', () => {
render(<LoginForm onSubmit={onSubmit} />);
expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
it('should show validation errors on empty submit', async () => {
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(await screen.findByText(/username is required/i)).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
it('should submit with valid data', async () => {
render(<LoginForm onSubmit={onSubmit} />);
await userEvent.type(screen.getByLabelText(/username/i), 'john');
await userEvent.type(screen.getByLabelText(/password/i), 'password123');
await userEvent.click(screen.getByRole('button', { name: /login/i }));
expect(onSubmit).toHaveBeenCalledWith({
username: 'john',
password: 'password123',
});
});
it('should handle password visibility toggle', () => {
render(<LoginForm onSubmit={onSubmit} />);
const passwordInput = screen.getByLabelText(/password/i);
expect(passwordInput).toHaveAttribute('type', 'password');
fireEvent.click(screen.getByRole('button', { name: /show password/i }));
expect(screen.getByLabelText(/password/i)).toHaveAttribute('type', 'text');
});
});
// ✅ Hooks 测试
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('should increment', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('should decrement', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
it('should reset to custom value', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});
7.2.2 测试最佳实践
// ✅ 测试应该像用户一样使用组件
// ❌ 不要
const badTest = () => {
const instance = new MyComponent();
instance.setState({ name: 'John' });
expect(instance.state.name).toBe('John');
};
// ✅ 正确
const goodTest = () => {
render(<MyComponent />);
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'John' } });
expect(screen.getByText('John')).toBeInTheDocument();
};
// ✅ 优先测试行为,而非实现细节
// ❌ 不要测试 state
// ✅ 测试渲染结果
// ✅ 使用 userEvent 而非 fireEvent(更接近真实用户交互)
// ❌ fireEvent.click(button);
// ✅ userEvent.click(button);
八、服务端渲染入门
8.1 Next.js 核心概念
8.1.1 Next.js 渲染模式
【架构决策】根据数据特性和用户体验选择合适的渲染策略
┌─────────────────────────────────────────────────────────────┐
│ │
│ SSR (Server-Side Rendering) │
│ ├── 每次请求:服务器 → HTML → 浏览器 │
│ ├── 适用:频繁变化、需要 SEO 的页面 │
│ └── 示例:新闻详情、社交媒体帖子 │
│ │
│ SSG (Static Site Generation) │
│ ├── 构建时:服务器 → HTML → 静态文件 │
│ ├── 适用:不频繁变化的内容 │
│ └── 示例:文档、博客文章、产品列表 │
│ │
│ ISR (Incremental Static Regeneration) │
│ ├── 构建 + 按需重新生成 │
│ ├── 适用:大量页面的中频更新 │
│ └── 示例:电商产品页、用户资料 │
│ │
│ CSR (Client-Side Rendering) │
│ ├── 浏览器 → JavaScript → 内容 │
│ ├── 适用:高度个性化、需登录的内容 │
│ └── 示例:仪表盘、个性化推荐 │
│ │
└─────────────────────────────────────────────────────────────┘
8.1.2 Next.js 13+ App Router 基础
// app/page.tsx - 首页(SSG,默认)
export default function HomePage() {
return <h1>Welcome to Next.js!</h1>;
}
// app/about/page.tsx - 关于页(SSG)
export default function AboutPage() {
return <h1>About Us</h1>;
}
// app/products/[id]/page.tsx - 动态路由(SSR)
// 生成:/products/1, /products/2, ...
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
// 服务端直接获取数据
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
// app/blog/[slug]/page.tsx - 博客文章(SSG + ISR)
export const revalidate = 3600; // 每小时重新验证
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getBlogPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
// 静态生成所有博客文章(构建时)
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
8.2 SSR/SSG 数据获取
8.2.1 服务端数据获取模式
// ✅ 服务端组件直接 async/await
async function getUser(id: string) {
const res = await fetch(`https://api.example.com/users/${id}`, {
// Next.js 默认缓存请求结果
// 需要动态数据时使用
cache: 'no-store', // 不缓存
// cache: 'force-cache' // 强制缓存(默认)
// next: { revalidate: 60 } // ISR 60秒后重新验证
});
if (!res.ok) throw new Error('Failed to fetch data');
return res.json();
}
// app/users/[id]/page.tsx
export default async function UserProfile({
params,
}: {
params: { id: string };
}) {
// 并行获取多个数据源
const [user, posts] = await Promise.all([
getUser(params.id),
getUserPosts(params.id),
]);
return (
<div>
<UserHeader user={user} />
<UserPosts posts={posts} />
</div>
);
}
8.2.2 客户端数据获取(CSR)
// ✅ 使用 useEffect(简单场景)
'use client';
import { useState, useEffect } from 'react';
export function UserData({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
const data = await getUser(userId);
setUser(data);
setLoading(false);
};
fetchUser();
}, [userId]);
if (loading) return <Skeleton />;
return <UserProfile user={user} />;
}
// ✅ 推荐:使用 SWR(客户端缓存)
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(res => res.json());
export function UserData({ userId }: { userId: string }) {
const { data: user, error, isLoading } = useSWR(
`/api/users/${userId}`,
fetcher,
{
revalidateOnFocus: false,
}
);
if (isLoading) return <Skeleton />;
if (error) return <Error />;
return <UserProfile user={user} />;
}
8.2.3 Hydration 问题与解决
// ✅ 避免 Hydration 不匹配
// ❌ 错误:在服务端和客户端渲染不同内容
const BadComponent = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 服务端:mounted = false
// 客户端:mounted = false(首次),然后 true(mount后)
// 导致 hydration mismatch
return <div>{mounted ? 'Client' : 'Server'}</div>;
};
// ✅ 正确:使用 useEffect 只在客户端执行
const GoodComponent = () => {
const [value, setValue] = useState('');
useEffect(() => {
// 只在客户端执行
setValue(localStorage.getItem('key') || '');
}, []);
return <div>{value}</div>;
};
// ✅ 使用 suppressHydrationWarning(必要时)
const DateComponent = ({ date }) => {
return (
<time dateTime={date} suppressHydrationWarning>
{new Date(date).toLocaleDateString()}
</time>
);
};
// ✅ 使用动态导入禁用 SSR
import dynamic from 'next/dynamic';
const NoSSRComponent = dynamic(
() => import('./HeavyComponent'),
{ ssr: false }
);
export function Page() {
return (
<div>
<h1>Static Content</h1>
<NoSSRComponent /> {/* 这个组件不会在服务端渲染 */}
</div>
);
}
附录:进阶学习路线图
📚 官方资源
- React 官方文档:https://react.dev (进阶内容:Concurrent Features、性能优化)
- React 源码分析:https://github.com/facebook/react (有兴趣深入的同学)
- React Team Blog:关注 React 新特性发布
🛠️ 推荐工具链(进阶)
- 状态管理:Zustand / Redux Toolkit / Jotai
- 数据获取:TanStack Query / SWR
- 样式:Tailwind CSS / CSS Modules
- 构建:Vite / Turbopack
- 类型检查:TypeScript 5+
- 测试:Vitest + React Testing Library
📖 进阶方向(完成进阶层后)
- React 源码探索:Fiber、Reconciler、Scheduler 源码分析
- 性能调优:Profiling、Chrome DevTools Performance 面板
- 微前端架构:qiankun / Module Federation
- Server Components:Next.js App Router 深度应用
- React Native:跨平台移动开发
- 状态机:XState 状态管理
- 可视化:D3.js / Three.js / React-Three-Fiber
- 动画:Framer Motion / GSAP
🎯 实战项目建议(进阶)
Level 5:团队协作中后台(2-4周)
- 目标:使用 TypeScript + Zustand + React Query + Tailwind CSS
- 要点:复杂表单、权限管理、数据可视化、单元测试
Level 6:SSR 应用(2-3周)
- 目标:掌握 Next.js App Router + 数据获取策略
- 要点:SSR/SSG/ISR 选型、缓存策略、Hydration
Level 7:性能优化专项(1-2周)
- 目标:实际优化一个慢应用, Lighthouse 分数 60 → 90+
- 要点:Code Splitting、虚拟列表、缓存策略、Bundle 优化
总结
本文档从资深 React 架构师的视角,系统地梳理了 React 进阶开发的核心知识体系:
✅ 五大核心突破
-
底层原理:
- Fiber 架构实现异步可中断渲染
- Diff 算法三假设实现 O(n) 复杂度
- Hooks 链表设计实现状态管理
- 合成事件系统的跨平台抽象
-
性能优化:
- React.memo + useCallback 防止无效渲染
- 虚拟列表处理长列表
- Code Splitting + Lazy Loading
- 构建优化(Tree Shaking、分包)
-
状态管理:
- Zustand/Jotai/Redux Toolkit 选型
- React Query/SWR 处理服务器状态
- 三层状态架构(服务器/全局/本地)
-
TypeScript 集成:
- Props/Hooks 类型定义
- 类型工程化
- 类型守卫与工具类型
-
服务端渲染:
- Next.js App Router
- SSR/SSG/ISR 渲染策略
- Hydration 问题解决
🎯 核心理念
- 原理驱动:理解底层才能写出高效代码
- 测量先行:用 Profiler 找瓶颈,而非猜测
- 渐进增强:从简单方案开始,按需升级
- 工程化保障:TypeScript + ESLint + 测试 = 可靠代码
📈 成长路径
Level 1-2: 熟练使用 API,完成基础项目
↓
Level 3-4: 理解原理,能解决复杂问题
↓
Level 5-6: 架构设计,性能优化
↓
Level 7+: 源码贡献,框架设计
下一步行动建议:
- 完成进阶层文档学习,重点关注第一部分的底层原理
- 完成 Level 5 的团队协作项目(TypeScript 全家桶)
- 学习 Next.js,完成 Level 6 的 SSR 项目
- 尝试性能优化实战,用 Profiler 验证效果
版本:v1.0
最后更新:2026-05-10
前置要求:React 基础层文档(已完成)
适用版本:React 18+ / Next.js 14+ / TypeScript 5+
作者视角:基于 5 年+ React 大型项目实战 + 源码分析经验整理
更多推荐

所有评论(0)