前言
💡 痛点: React 18 的 Concurrent Mode 改变了什么?useTransition/useDeferredValue 怎么选?Server Components 和 Client Components 如何划分?Suspense 的边界在哪里?
🎯 解决方案: 从新特性→Hooks进阶→状态管理→性能优化→TypeScript→架构模式,系统掌握 React 18。
#mermaid-svg-T2aLbtSLYf2bswEA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-T2aLbtSLYf2bswEA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-T2aLbtSLYf2bswEA .error-icon{fill:#552222;}#mermaid-svg-T2aLbtSLYf2bswEA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-T2aLbtSLYf2bswEA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-T2aLbtSLYf2bswEA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .marker.cross{stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-T2aLbtSLYf2bswEA p{margin:0;}#mermaid-svg-T2aLbtSLYf2bswEA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label text{fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label span{color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster-label span p{background-color:transparent;}#mermaid-svg-T2aLbtSLYf2bswEA .label text,#mermaid-svg-T2aLbtSLYf2bswEA span{fill:#333;color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .node rect,#mermaid-svg-T2aLbtSLYf2bswEA .node circle,#mermaid-svg-T2aLbtSLYf2bswEA .node ellipse,#mermaid-svg-T2aLbtSLYf2bswEA .node polygon,#mermaid-svg-T2aLbtSLYf2bswEA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .rough-node .label text,#mermaid-svg-T2aLbtSLYf2bswEA .node .label text,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label,#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label{text-anchor:middle;}#mermaid-svg-T2aLbtSLYf2bswEA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .rough-node .label,#mermaid-svg-T2aLbtSLYf2bswEA .node .label,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label,#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label{text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .node.clickable{cursor:pointer;}#mermaid-svg-T2aLbtSLYf2bswEA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .arrowheadPath{fill:#333333;}#mermaid-svg-T2aLbtSLYf2bswEA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-T2aLbtSLYf2bswEA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-T2aLbtSLYf2bswEA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster text{fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA .cluster span{color:#333;}#mermaid-svg-T2aLbtSLYf2bswEA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-T2aLbtSLYf2bswEA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-T2aLbtSLYf2bswEA rect.text{fill:none;stroke-width:0;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape p,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-T2aLbtSLYf2bswEA .icon-shape .label rect,#mermaid-svg-T2aLbtSLYf2bswEA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-T2aLbtSLYf2bswEA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-T2aLbtSLYf2bswEA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-T2aLbtSLYf2bswEA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 状态管理
React 18 新特性
服务端渲染
Hooks 进阶
useMemo
记忆化
useReducer
复杂状态
useCallback
回调缓存
useContext
上下文
useRef
可变引用
Suspense
Suspense
加载边界
Suspense List
编排顺序
并发渲染
Concurrent Mode
并发渲染
useTransition
过渡更新
useDeferredValue
延迟值
React 18 SSR
Streaming SSR
Server Components
服务端组件
useId/useSyncExternalStore
新 Hooks
Zustand
轻量状态
Jotai
原子化
Redux Toolkit
标准实践
React 18 vs React 17 核心差异:
| 特性 | React 17 | React 18 | 收益 |
|---|---|---|---|
| 并发渲染 | ❌ 不支持 | ✅ Concurrent Mode | 更流畅的 UI |
| Suspense | ⚠️ 基础支持 | ✅ Streaming SSR | 流式加载 |
| Automatic Batching | ❌ 需手动 | ✅ 自动批处理 | 减少渲染 |
| Server Components | ❌ 无 | ✅ 支持 | 零客户端 JS |
| 新 Hooks | - | useTransition/useDeferredValue/useId | 更细粒度控制 |
| Strict Mode | 部分检查 | 双重调用+严格检查 | 开发期暴露问题 |
一、React 18 新特性
1.1 Concurrent Mode 并发渲染
tsx
// ===== 并发渲染原理 =====
// React 18 之前:渲染是同步、不可中断的
// React 18:渲染可以被中断、优先级调度
// setState 不再阻塞 UI
// React 可以同时处理多个更新,根据优先级决定渲染顺序
// 渲染优先级:用户交互 > 过渡更新 > 后台预渲染
import { useState, useTransition } from 'react';
// useTransition:将某些更新标记为"过渡更新"
// 过渡更新可以被更高优先级的交互打断
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState([]);
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const newQuery = e.target.value;
setQuery(newQuery);
// 将数据库查询标记为过渡更新
startTransition(() => {
setResults(searchDatabase(newQuery)); // 可以被打断
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
{isPending ? <Spinner /> : <ResultsList results={results} />}
</div>
);
}
// useDeferredValue:延迟非紧急更新
function SearchBar() {
const [text, setText] = useState('');
// text 作为状态更新
// deferredText 延迟更新,可能比 text "旧"
const deferredText = useDeferredValue(text);
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
{/* deferredText 渲染时可能显示旧数据 */}
<SlowList text={deferredText} />
</div>
);
}
// ===== Automatic Batching 自动批处理 =====
// React 18 之前:多个 setState 只有在事件处理函数结束后才批量更新
// React 18:所有 setState 自动批处理(包括 Promise/setTimeout)
// React 17
function handleClick() {
fetch('/api/user').then(() => {
setLoading(false); // 触发一次重渲染
setError(null); // 触发一次重渲染(不会合并!)
});
}
// React 18:自动批处理,fetch 完成后只触发一次重渲染
function handleClick() {
fetch('/api/user').then(() => {
setLoading(false);
setError(null);
// → 只触发一次重渲染
});
}
// flushSync:强制同步更新
import { flushSync } from 'react-dom';
function handleUpload(file: File) {
flushSync(() => {
setUploading(true);
});
// 这个 setState 不会和上面的合并
flushSync(() => {
setFile(file);
});
}
1.2 Suspense 与 Streaming SSR
tsx
// ===== Suspense 边界 =====
// Suspense 组件:当子组件还在加载时,显示 fallback
import { Suspense } from 'react';
function App() {
return (
<>
{/* 数据加载 */}
<Suspense fallback={<UserSkeleton />}>
<UserProfile userId={userId} />
</Suspense>
{/* 多个 Suspense,独立的加载状态 */}
<Suspense fallback={<PostsSkeleton />}>
<UserPosts userId={userId} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<UserComments userId={userId} />
</Suspense>
</>
);
}
// ===== SuspenseList 编排 =====
// 控制多个 Suspense 的显示顺序
import { Suspense, SuspenseList } from 'react';
function Dashboard() {
return (
<SuspenseList revealOrder="forwards">
{/* 其他内容先显示 */}
<Suspense fallback={<HeaderSkeleton />}>
<DashboardHeader />
</Suspense>
{/* 按顺序显示 */}
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</SuspenseList>
);
}
// revealOrder: "together" | "forwards" | "backwards"
// ===== Streaming SSR =====
// 服务端流式渲染:HTML 分块发送
// 1. 服务端立即返回 HTML shell
// 2. JS bundle 加载时,React 在服务端渲染剩余内容
// 3. Streaming 到客户端
// app/routes/page.tsx(Remix/Next.js App Router)
export async function loader() {
const data = await fetchDashboardData();
return json(data);
}
// 服务端组件(RSC)
async function DashboardPage() {
// 服务端直接查询数据
const data = await getDashboardData();
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart data={data.chart} />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders data={data.orders} />
</Suspense>
</div>
);
}
二、Hooks 进阶
2.1 useState 与状态模式
tsx
// ===== useState 进阶用法 =====
import { useState, useCallback } from 'react';
// 1. 函数式更新(基于前一个状态)
const [count, setCount] = useState(0);
setCount(prev => prev + 1); // ✅ 推荐
setCount(count + 1); // ⚠️ 可能过时
// 2. 延迟初始化(复杂初始值)
const [data, setData] = useState(() => {
// 只在首次渲染时执行
const stored = localStorage.getItem('data');
return stored ? JSON.parse(stored) : initialData;
});
// 3. 复合状态(相关状态打包)
// ❌ 不推荐:两个相关状态
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// ✅ 推荐:打包成对象
const [name, setName] = useState({ first: '', last: '' });
setName(prev => ({ ...prev, first: 'John' }));
// 4. useReducer 复杂状态
type State = { count: number; step: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return { count: 0, step: 1 };
}
}
const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });
// 5. Immer + useReducer(不可变更新简化)
import { useImmerReducer } from 'use-immer';
const [state, dispatch] = useImmerReducer((draft, action) => {
switch (action.type) {
case 'updateUser':
draft.user.name = action.payload.name;
break;
case 'addPost':
draft.posts.push(action.payload);
break;
}
}, initialState);
2.2 useEffect 深度解析
tsx
// ===== useEffect 依赖控制 =====
import { useEffect, useRef } from 'react';
// 1. 清理函数
useEffect(() => {
const timer = setInterval(() => {
setTime(Date.now());
}, 1000);
// 返回清理函数
return () => clearInterval(timer);
}, []);
// 2. 依赖数组控制
// []:只在挂载/卸载时执行
// [value]:value 变化时执行
// 无依赖:每次渲染后都执行(⚠️ 慎用)
// 3. 异步 useEffect(需要包装)
useEffect(() => {
async function fetchData() {
const res = await fetch(`/api/user/${userId}`);
const data = await res.json();
setUser(data);
}
fetchData();
}, [userId]);
// 4. 条件执行 useEffect
useEffect(() => {
if (!userId) return;
const controller = new AbortController();
fetch(`/api/user/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(setUser);
return () => controller.abort();
}, [userId]);
// 5. Ref 版本(存储最新值,不触发重新执行)
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// 使用
const prevCount = usePrevious(count);
// prevCount 更新但不会触发额外渲染
2.3 useCallback 与 useMemo
tsx
// ===== useCallback vs useMemo =====
import { useState, useCallback, useMemo, memo } from 'react';
// useCallback:缓存函数引用
// 用于:作为 props 传递给子组件、作为 useEffect 依赖、作为事件处理器
const handleClick = useCallback((id: string) => {
setSelectedId(id);
}, []); // 依赖为空,函数引用永远不变
// useMemo:缓存计算结果
// 用于:复杂计算、对象/数组字面量、组件引用
const sortedItems = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => b.score - a.score);
}, [items]); // items 变化才重新计算
// ===== 配合 memo 优化子组件 =====
// 子组件:只有 props 变化时才重新渲染
const ExpensiveList = memo(function ExpensiveList({
items,
onSelect
}: {
items: Item[];
onSelect: (id: string) => void;
}) {
return (
<ul>
{items.map(item => (
<li key={item.id} onClick={() => onSelect(item.id)}>
{item.name}
</li>
))}
</ul>
);
});
// 父组件:传入 useCallback 包装的函数
function Parent() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState<string | null>(null);
// items 变化时生成新的 onSelect
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
}, []);
// derivedItems 变化时才重新创建 ExpensiveList
const derivedItems = useMemo(
() => items.filter(i => i.type === 'active'),
[items]
);
return (
<ExpensiveList
items={derivedItems}
onSelect={handleSelect}
/>
);
}
// ===== 不要过度优化 =====
// ❌ 过度优化:每次渲染都创建新对象
const style = useMemo(() => ({ color: 'red' }), []);
const options = useMemo(() => ({ limit: 10 }), []);
// ✅ 简单值不需要 useMemo
const style = { color: 'red' }; // 字面量不会变
const options = { limit: 10 };
// ✅ 只有复杂计算或昂贵操作才需要
const sorted = useMemo(() => {
return bigArray.sort((a, b) => b.timestamp - a.timestamp);
}, [bigArray]);
三、自定义 Hooks
3.1 基础自定义 Hooks
tsx
// ===== useLocalStorage =====
import { useState, useEffect, useCallback } from 'react';
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
// SSR 兼容
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') return initialValue;
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
// 同步到 localStorage
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('localStorage error:', error);
}
}, [key, storedValue]);
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue(prev => {
const newValue = value instanceof Function ? value(prev) : value;
return newValue;
});
}, []);
return [storedValue, setValue];
}
// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');
// ===== useDebounce =====
import { useState, useEffect } from 'react';
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
// 使用
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
fetchResults(debouncedQuery);
}
}, [debouncedQuery]);
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
// ===== useFetch =====
import { useState, useEffect } from 'react';
interface FetchState<T> {
data: T | null;
error: Error | null;
isLoading: boolean;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
error: null,
isLoading: true,
});
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
setState(prev => ({ ...prev, isLoading: true, error: null }));
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setState({ data, error: null, isLoading: false });
} catch (error: any) {
if (error.name !== 'AbortError') {
setState({ data: null, error, isLoading: false });
}
}
}
fetchData();
return () => controller.abort();
}, [url]);
return state;
}
// 使用
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useFetch<User>(
`/api/users/${userId}`
);
if (isLoading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return <div>{user.name}</div>;
}
3.2 复杂业务 Hooks
tsx
// ===== usePagination =====
import { useState, useMemo } from 'react';
interface PaginationResult<T> {
currentPage: number;
pageSize: number;
total: number;
totalPages: number;
currentItems: T[];
pageNumbers: number[];
goToPage: (page: number) => void;
nextPage: () => void;
prevPage: () => void;
isFirst: boolean;
isLast: boolean;
}
function usePagination<T>(
items: T[],
initialPageSize = 10
): PaginationResult<T> {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(initialPageSize);
const total = items.length;
const totalPages = Math.ceil(total / pageSize);
const currentItems = useMemo(() => {
const start = (currentPage - 1) * pageSize;
return items.slice(start, start + pageSize);
}, [items, currentPage, pageSize]);
const pageNumbers = useMemo(() => {
const pages: number[] = [];
const delta = 2; // 当前页两侧显示的页数
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= currentPage - delta && i <= currentPage + delta)
) {
pages.push(i);
} else if (pages[pages.length - 1] !== '...') {
pages.push('...' as any);
}
}
return pages;
}, [currentPage, totalPages]);
const goToPage = (page: number) => {
setCurrentPage(Math.max(1, Math.min(page, totalPages)));
};
const nextPage = () => goToPage(currentPage + 1);
const prevPage = () => goToPage(currentPage - 1);
return {
currentPage,
pageSize,
total,
totalPages,
currentItems,
pageNumbers,
goToPage,
nextPage,
prevPage,
isFirst: currentPage === 1,
isLast: currentPage === totalPages,
};
}
// ===== useIntersectionObserver =====
import { useEffect, useRef, useState } from 'react';
interface UseIntersectionObserverOptions {
threshold?: number;
root?: Element | null;
rootMargin?: string;
freezeOnceVisible?: boolean;
}
function useIntersectionObserver(
options: UseIntersectionObserverOptions = {}
): [React.RefObject<HTMLDivElement>, boolean] {
const { threshold = 0, root = null, rootMargin = '0px', freezeOnceVisible = false } = options;
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const element = ref.current;
if (!element) return;
if (freezeOnceVisible && isVisible) return;
const observer = new IntersectionObserver(
([entry]) => {
setIsVisible(entry.isIntersecting);
},
{ threshold, root, rootMargin }
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, root, rootMargin, freezeOnceVisible, isVisible]);
return [ref, isVisible];
}
// 使用:无限滚动
function InfiniteList() {
const [ref, isVisible] = useIntersectionObserver({ threshold: 0.1 });
const [items, setItems] = useState(initialItems);
useEffect(() => {
if (isVisible && hasMore) {
loadMore();
}
}, [isVisible]);
return (
<div>
{items.map(item => <Item key={item.id} data={item} />)}
<div ref={ref}>
{isLoading && <Spinner />}
</div>
</div>
);
}
四、状态管理
4.1 Zustand 轻量状态
tsx
// ===== Zustand 基础 =====
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
// 定义 Store 类型
interface UserState {
user: User | null;
isAuthenticated: boolean;
login: (user: User) => void;
logout: () => void;
}
const useUserStore = create<UserState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false }),
}),
{ name: 'user-storage' }
)
);
// ===== 派生状态(Selector)=====
// 组件中:自动订阅,仅 user 变化时重新渲染
function UserProfile() {
const user = useUserStore((state) => state.user);
return <div>{user?.name}</div>;
}
// 派生状态:计算属性
const useUserName = () => useUserStore((state) => state.user?.name ?? 'Guest');
// 多个订阅:选择多个值
function UserBadge() {
const { user, isAuthenticated } = useUserStore(
useShallow((state) => ({
user: state.user,
isAuthenticated: state.isAuthenticated,
}))
);
return isAuthenticated ? <Badge>{user?.name}</Badge> : null;
}
// ===== 切片模式(大型应用)=====
// stores/userSlice.ts
const createUserSlice = (set) => ({
user: null,
login: (user) => set({ user }),
logout: () => set({ user: null }),
});
// stores/uiSlice.ts
const createUISlice = (set) => ({
sidebarOpen: false,
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
});
// stores/index.ts
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useStore = create(
devtools(
(...a) => ({
...createUserSlice(...a),
...createUISlice(...a),
})
)
);
// ===== 异步 Actions =====
interface PostState {
posts: Post[];
isLoading: boolean;
error: string | null;
fetchPosts: () => Promise<void>;
createPost: (data: CreatePostInput) => Promise<void>;
}
const usePostStore = create<PostState>()((set, get) => ({
posts: [],
isLoading: false,
error: null,
fetchPosts: async () => {
set({ isLoading: true, error: null });
try {
const posts = await api.get('/posts');
set({ posts, isLoading: false });
} catch (error: any) {
set({ error: error.message, isLoading: false });
}
},
createPost: async (data) => {
const newPost = await api.post('/posts', data);
set((state) => ({ posts: [newPost, ...state.posts] }));
},
}));
4.2 React Query 数据获取
tsx
// ===== TanStack Query(React Query)=====
import { QueryClient, QueryClientProvider, useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// 创建客户端
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 分钟内不重新请求
gcTime: 10 * 60 * 1000, // 缓存保留 10 分钟
retry: 3,
refetchOnWindowFocus: false,
},
},
});
// 基础查询
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => api.get(`/users/${userId}`),
enabled: !!userId, // userId 为空时不请求
});
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <Profile user={data} />;
}
// 变异(POST/PUT/DELETE)
function CreatePost() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (newPost: CreatePostInput) =>
api.post('/posts', newPost),
onSuccess: () => {
// invalidate 重新获取列表
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
onError: (error) => {
console.error('Create failed:', error);
},
});
function handleSubmit(data: CreatePostInput) {
mutation.mutate(data);
}
return (
<form onSubmit={handleSubmit}>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? '提交中...' : '发布'}
</button>
</form>
);
}
// 乐观更新
function ToggleTodo() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ id, completed }: { id: string; completed: boolean }) =>
api.patch(`/todos/${id}`, { completed }),
onMutate: async ({ id, completed }) => {
// 取消所有 incoming 请求
await queryClient.cancelQueries({ queryKey: ['todos'] });
// 保存旧数据
const previousTodos = queryClient.getQueryData(['todos']);
// 乐观更新
queryClient.setQueryData(['todos'], (old: Todo[]) =>
old.map(todo =>
todo.id === id ? { ...todo, completed } : todo
)
);
return { previousTodos };
},
onError: (err, variables, context) => {
// 失败时回滚
queryClient.setQueryData(['todos'], context?.previousTodos);
},
onSettled: () => {
// 重新同步
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<TodoItem
todo={todo}
onToggle={() => mutation.mutate({ id: todo.id, completed: !todo.completed })}
/>
);
}
五、性能优化
5.1 React.memo 与组件优化
tsx
// ===== React.memo =====
import { memo, useMemo, useCallback } from 'react';
// 浅比较 Props
const Button = memo(function Button({
onClick,
children
}: {
onClick: () => void;
children: React.ReactNode;
}) {
console.log('Button rendered');
return <button onClick={onClick}>{children}</button>;
});
// 自定义比较函数(深度比较)
const ListItem = memo(
function ListItem({ item, onSelect }: ListItemProps) {
return (
<div onClick={() => onSelect(item.id)}>
{item.name}
</div>
);
},
(prevProps, nextProps) => {
// 返回 true 表示 props 没变,不需要重新渲染
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name &&
prevProps.onSelect === nextProps.onSelect
);
}
);
// ===== 列表渲染优化 =====
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(user => (
// 每个 ListItem 都是 memo 的
<ListItem key={user.id} item={user} />
))}
</ul>
);
}
// ===== useMemo 优化列表 =====
function FilteredUserList({ users, filter }: Props) {
// 只有 users 或 filter 变化时才重新计算
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
return <UserList users={filteredUsers} />;
}
5.2 代码分割与懒加载
tsx
// ===== React.lazy 懒加载 =====
import { lazy, Suspense } from 'react';
// 路由懒加载
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// 组件懒加载
const HeavyChart = lazy(() => import('./components/HeavyChart'));
// Suspense 包裹
function App() {
return (
<Routes>
<Route path="/dashboard" element={
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
} />
</Routes>
);
}
// ===== 带错误边界的懒加载 =====
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<PageSkeleton />}>
<HeavyComponent />
</Suspense>
</ErrorBoundary>
);
}
// ===== 预加载 =====
// Hover 时预加载
function Navigation() {
return (
<nav>
<Link
to="/dashboard"
onMouseEnter={() => {
// 用户 hover 时预加载
import('./pages/Dashboard');
}}
>
Dashboard
</Link>
</nav>
);
}
六、TypeScript 与 React
6.1 Props 与 Events 类型
tsx
// ===== 基础 Props 类型 =====
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
children: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
function Button({
variant = 'primary',
size = 'md',
children,
onClick,
disabled,
loading,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled || loading}
>
{loading ? <Spinner /> : children}
</button>
);
}
// ===== Generic Props =====
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用
<List
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>
// ===== 事件处理器类型 =====
// 常用事件类型
onChange: React.ChangeEventHandler<HTMLInputElement>
onSubmit: React.FormEventHandler<HTMLFormElement>
onClick: React.MouseEventHandler<HTMLButtonElement>
onKeyDown: React.KeyboardEventHandler<HTMLDivElement>
onFocus: React.FocusEventHandler<HTMLInputElement>
onScroll: React.UIEventHandler<HTMLDivElement>
// Form 事件
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
}
// Input 事件
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
const checked = (e.target as HTMLInputElement).checked;
}
6.2 Context 类型安全
tsx
// ===== 类型安全的 Context =====
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
setTheme: (theme: 'light' | 'dark') => void;
}
const ThemeContext = React.createContext<ThemeContextValue | null>(null);
function useTheme(): ThemeContextValue {
const context = React.useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
// 使用
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return <button onClick={toggleTheme}>{theme === 'light' ? '🌙' : '☀️'}</button>;
}
// Provider
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const value = useMemo(() => ({
theme,
toggleTheme: () => setTheme(t => t === 'light' ? 'dark' : 'light'),
setTheme,
}), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
七、总结
React 18 新特性决策树
更新是否是紧急的?
├── 是(用户输入、点击)→ 直接 setState
└── 否(数据列表、搜索过滤)
└── 是否需要等待数据?→ useTransition
├── 需要 → useTransition + isPending
└── 不需要 → useDeferredValue
Hooks 选择指南
| 场景 | Hook | 说明 |
|---|---|---|
| 基础状态 | useState | 简单值 |
| 复杂状态 | useReducer | 多个相关状态 |
| 副作用 | useEffect | DOM 操作、订阅、定时器 |
| 记忆函数 | useCallback | 作为 props 传递的回调 |
| 记忆计算 | useMemo | 昂贵计算 |
| DOM 引用 | useRef | 不想触发渲染的可变值 |
| 全局状态 | Zustand/Context | 跨组件共享 |
| 服务端数据 | React Query | 请求缓存、乐观更新 |
| 防抖 | 自定义 | useDebounce |
| 分页 | 自定义 | usePagination |
| 懒加载 | React.lazy | 减少首屏包体积 |
性能优化清单
| 检查项 | 优化方向 |
|---|---|
| 列表渲染 | key + React.memo |
| 回调传递 | useCallback |
| 派生计算 | useMemo |
| 条件渲染 | 提前 return + 拆分组件 |
| 代码分割 | React.lazy + Suspense |
| 状态位置 | 提升到需要的最上层 |
| 重新渲染 | React DevTools Profiler 定位 |
本文涵盖 React 18 完整知识:Concurrent Mode 原理 + useTransition/useDeferredValue + Automatic Batching + Streaming SSR/Suspense + Hooks 进阶(useReducer + useEffect + useCallback/useMemo)+ 自定义 Hooks(useLocalStorage/useDebounce/useFetch/usePagination/useIntersectionObserver)+ Zustand 轻量状态管理 + React Query 数据获取(乐观更新)+ 性能优化(memo + 懒加载)+ TypeScript 类型安全(泛型组件 + 事件类型 + Context)。