React 专业技能
面向生产环境 React 应用的 TypeScript 开发参考。
适用场景
- 编写或审查 React 组件、Hooks、页面
- 设计状态模型或组件架构
- 使用 Server Components / Client Components(Next.js App Router)
- 实现表单、数据获取、实时功能
- 优化渲染性能或打包体积
- 确保无障碍合规
0. 版本感知与约束
关键:先检测项目 React 版本
应用任何模式前,先确认项目实际 React 版本:
# 检查 React 版本
grep '"react"' package.json
# 或
cat node_modules/react/package.json | grep '"version"'
版本兼容性矩阵
| 特性 | React 16.8+ | React 17+ | React 18+ | React 19+ |
|------|:-----------:|:---------:|:---------:|:---------:|
| Hooks(useState、useEffect 等) | ✅ | ✅ | ✅ | ✅ |
| Fragments、Portals | ✅ | ✅ | ✅ | ✅ |
| Context(createContext) | ✅ | ✅ | ✅ | ✅ |
| Lazy + Suspense(代码分割) | ✅ | ✅ | ✅ | ✅ |
| 新 JSX 转换(无需 import React) | ❌ | ✅ | ✅ | ✅ |
| 并发特性(useTransition、useDeferredValue) | ❌ | ❌ | ✅ | ✅ |
| Suspense 数据获取 | ❌ | ❌ | ✅ | ✅ |
| useId | ❌ | ❌ | ✅ | ✅ |
| useSyncExternalStore | ❌ | ❌ | ✅ | ✅ |
| Server Components(RSC) | ❌ | ❌ | ✅* | ✅ |
| useActionState | ❌ | ❌ | ❌ | ✅ |
| useOptimistic | ❌ | ❌ | ❌ | ✅ |
| use() hook | ❌ | ❌ | ❌ | ✅ |
| Form Actions(<form action={}> ) | ❌ | ❌ | ❌ | ✅ |
| ref 作为 prop(无需 forwardRef) | ❌ | ❌ | ❌ | ✅ |
*React 18 的 RSC 需要 Next.js 13+ App Router 或自定义流式渲染。
规则
- 禁止使用超出项目 React 版本的特性。 项目为 React 17 时,不得使用
useTransition、Suspense 数据获取或 RSC。 - 同时检查
next版本 — RSC 模式需要 Next.js 13+ App Router;Pages Router 项目不能使用。 - 不确定时,先询问。 不要假设项目已升级。
静态资源 — 禁止修改
以下文件视为不可变,绝不能修改:
public/目录(图片、字体、favicon、manifest、robots.txt)- 静态资源文件:
*.png、*.jpg、*.jpeg、*.gif、*.svg、*.ico、*.webp、*.avif - 字体文件:
*.woff、*.woff2、*.ttf、*.otf、*.eot - 音视频:
*.mp4、*.webm、*.mp3、*.ogg - 构建产物:
.next/、dist/、build/、out/ - 锁文件:
package-lock.json、yarn.lock、pnpm-lock.yaml
如果任务需要修改静态资源(替换图片、更新字体),必须向用户确认后才能操作。
1. 核心原则
| 原则 | 规则 |
|------|------|
| 纯函数 | 渲染是 props + state 的纯函数。在渲染中派生值,不要通过 useEffect 同步。 |
| 不可变性 | 始终使用展开运算符({...obj} / [...arr])。禁止直接修改 state。 |
| 组合优先 | 通过 children、render props 或组件 props 组合。不用继承。 |
| KISS | 能用最简单的方案就用最简单的。不要过度抽象。 |
| YAGNI | 不需要就不要建。 |
2. TypeScript React 规范
命名
// 组件:PascalCase
export function MarketCard() {}
// Hooks:camelCase + "use" 前缀
export function useMarketData() {}
// Props 接口:组件名 + "Props"
interface MarketCardProps {
market: Market
onSelect: (id: string) => void
}
// 事件处理器:"handle" + 事件
const handleClick = () => {}
// 布尔 props:"is" / "has" / "should" 前缀
interface Props {
isLoading: boolean
hasError: boolean
}
类型安全
// 推荐:可区分联合类型管理状态
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
// 推荐:const 断言用于配置
const ROUTES = {
home: '/',
markets: '/markets',
profile: '/profile',
} as const
// 禁止:不使用 `any` — 用 `unknown` + 类型收窄
function process(input: unknown) {
if (typeof input === 'string') { /* 安全 */ }
}
文件组织
src/
├── app/ # Next.js App Router 页面
├── components/
│ ├── ui/ # 通用组件:Button、Input、Modal
│ ├── domain/ # 业务组件:MarketCard、TradeForm
│ └── layouts/ # 布局组件
├── hooks/ # 自定义 Hooks
├── lib/ # 工具函数、API 客户端、常量
├── types/ # 共享 TypeScript 类型
└── styles/ # 全局样式
3. 组件模式
通过 Children 组合
<Card>
<CardHeader>标题</CardHeader>
<CardBody>内容</CardBody>
</Card>
复合组件(通过 Context 共享状态)
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">个人信息</Tabs.Trigger>
<Tabs.Trigger value="settings">设置</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile"><Profile /></Tabs.Panel>
<Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>
命名插槽
<Page header={<Nav />} sidebar={<Filters />}>
<Results />
</Page>
Render Props(父组件需向输出传参时)
<DataLoader id={id}>
{({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
现代替代方案:用 Hook(useData(id))返回相同结构 — 通常更简洁。
Props 接口模式
interface ButtonProps {
children: React.ReactNode
onClick: () => void
disabled?: boolean
variant?: 'primary' | 'secondary' | 'ghost'
}
export function Button({
children,
onClick,
disabled = false,
variant = 'primary',
}: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={`btn-${variant}`}>
{children}
</button>
)
}
4. Hooks 纪律
规则
- 只在顶层调用,不在条件或循环内
useEffect的返回函数中清理所有订阅、定时器、监听器- 新状态依赖旧状态时使用函数式更新(
setX(prev => prev + 1)) - 默认不做 memo — 只在性能分析工具证明有必要时才加
useMemo/useCallback - 仅当相同 Hook 序列出现在 2+ 组件中时才提取自定义 Hook
派生状态 — 计算而非同步
// 正确:渲染时直接计算
function Cart({ items }: { items: CartItem[] }) {
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0)
return <span>{formatMoney(total)}</span>
}
// 错误:通过 useEffect 同步(多一次渲染,可能不同步)
function Cart({ items }: { items: CartItem[] }) {
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((sum, i) => sum + i.price * i.qty, 0))
}, [items])
return <span>{formatMoney(total)}</span>
}
可复用 Hook 示例
// 防抖
function useDebounce<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(id)
}, [value, delay])
return debounced
}
// 切换
function useToggle(initial = false): [boolean, () => void] {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle]
}
5. 状态架构
决策树
仅一个组件使用?
→ 该组件内 useState
父组件 + 少量子组件使用?
→ 提升到最近的公共祖先
跨越远距离分支、低频读取(主题、认证、语言)?
→ React Context
高频更新且跨树共享?
→ 外部 store(Zustand、Jotai、Redux Toolkit)
来源于服务端?
→ 服务端状态库(TanStack Query、SWR、RSC fetch)
Context + Reducer(中等复杂度)
type Action =
| { type: 'SET_ITEMS'; payload: Item[] }
| { type: 'SELECT'; payload: Item }
| { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_ITEMS': return { ...state, items: action.payload }
case 'SELECT': return { ...state, selected: action.payload }
case 'SET_LOADING': return { ...state, loading: action.payload }
}
}
const Ctx = createContext<{ state: State; dispatch: Dispatch<Action> } | undefined>(undefined)
export function Provider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, initialState)
return <Ctx.Provider value={{ state, dispatch }}>{children}</Ctx.Provider>
}
export function useStore() {
const ctx = useContext(Ctx)
if (!ctx) throw new Error('useStore 必须在 Provider 内使用')
return ctx
}
拆分 Context 防止渲染级联
const ThemeContext = createContext<Theme>('light')
const NotificationsContext = createContext<Notification[]>([])
// 只消费 ThemeContext 的组件不会因 notifications 变化而重渲染
6. Server / Client Components(RSC)
⚠️ 版本门控:需要 React 18 + Next.js 13+ App Router,或 React 19+。React 17 或 Pages Router 项目跳过本节。
边界规则
- Server Component(默认):async、不发送 JS、可直接访问 DB/API
- Client Component:用
"use client"声明,用于交互(onClick、useState 等) - Server → Client:传递可序列化的 props 或
children - Client → Server:通过
<form action={...}>调用 Server Actions - 禁止从 Client Component 文件中
importServer Component — 通过children组合
// Server Component
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } })
if (!product) notFound()
return <ProductView product={product} />
}
// Client Component
"use client"
export function AddToCartButton({ productId }: { productId: string }) {
const [pending, startTransition] = useTransition()
return (
<button
disabled={pending}
onClick={() => startTransition(() => addToCart(productId))}
>
{pending ? '添加中...' : '加入购物车'}
</button>
)
}
7. 数据获取
⚠️ Suspense 数据获取需要 React 18+。React 16/17 使用 useEffect + state 或 TanStack Query 非 Suspense 模式。
| 需求 | 工具 | 最低版本 |
|------|------|----------|
| 每次请求数据(Next.js App Router) | RSC await fetch() | React 18 + Next 13+ |
| 客户端缓存 + 变更 + 失效 | TanStack Query | 任意(<18 无 Suspense 模式) |
| 轻量重新验证 | SWR | 任意 |
| 实时数据 | SSE / WebSocket | 任意 |
| 一次性触发 | 事件处理器中 fetch() | 任意 |
反模式:useEffect + fetch 获取应用数据 — 存在竞态条件、无缓存、无重试、无 Suspense 集成。
Suspense + Error Boundary
<ErrorBoundary fallback={<ErrorView />}>
<Suspense fallback={<UserSkeleton />}>
<UserDetail id={id} />
</Suspense>
</ErrorBoundary>
Suspense 边界放在靠近数据的位置,而非路由根部 — 渐进式展示内容。
8. 表单
React 19 Form Actions(React 19+ 新代码首选)
⚠️ 版本门控:
useActionState和<form action={}>需要 React 19。React 18 及以下使用受控输入 + onSubmit 处理器。
"use client"
import { useActionState } from "react"
const initial = { error: null as string | null }
async function updateAction(_prev: typeof initial, formData: FormData) {
"use server"
const parsed = Schema.safeParse(Object.fromEntries(formData))
if (!parsed.success) return { error: "输入无效" }
await db.user.update({ where: { id: parsed.data.id }, data: parsed.data })
return { error: null }
}
export function UserForm() {
const [state, formAction, pending] = useActionState(updateAction, initial)
return (
<form action={formAction}>
<input name="name" required />
<button type="submit" disabled={pending}>保存</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
)
}
受控输入
当值驱动其他 UI、每次按键格式化、或实现实时验证时使用。
复杂表单
多步骤、动态字段数组、跨字段校验 → 使用库(React Hook Form、TanStack Form)。自行管理超过简单复杂度的表单状态是维护陷阱。
Zod 校验
import { z } from 'zod'
const CreateMarketSchema = z.object({
name: z.string().min(1).max(200),
description: z.string().min(1).max(2000),
endDate: z.string().datetime(),
categories: z.array(z.string()).min(1),
})
9. 性能优化
React.memo 何时有效
仅当以下三个条件同时满足时才包裹:
- 频繁重渲染
- props 在渲染间通常相同
- 渲染开销可被度量
否则相等性检查本身就是额外开销。
记忆化
// 昂贵计算
const sorted = useMemo(() => [...items].sort((a, b) => b.score - a.score), [items])
// 传递给 memo 子组件的回调
const handleSearch = useCallback((q: string) => setQuery(q), [])
代码分割
const HeavyChart = lazy(() => import('./HeavyChart'))
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
虚拟列表(列表项 > ~50 个非简单项时)
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map(row => (
<div
key={row.index}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${row.start}px)`,
height: `${row.size}px`,
width: '100%',
}}
>
<ItemCard item={items[row.index]} />
</div>
))}
</div>
</div>
)
}
异步并行获取
const [users, markets, stats] = await Promise.all([
fetchUsers(),
fetchMarkets(),
fetchStats(),
])
10. 无障碍
语义 HTML 优先
使用 <button>、<a>、<nav>、<main>、<dialog>,再考虑 role 属性。
键盘导航
function Dropdown({ options, onSelect }: DropdownProps) {
const [activeIndex, setActiveIndex] = useState(0)
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setActiveIndex(i => Math.min(i + 1, options.length - 1))
break
case 'ArrowUp':
e.preventDefault()
setActiveIndex(i => Math.max(i - 1, 0))
break
case 'Enter':
onSelect(options[activeIndex])
break
case 'Escape':
close()
break
}
}
return (
<div role="combobox" aria-expanded={isOpen} onKeyDown={handleKeyDown}>
{/* ... */}
</div>
)
}
焦点管理
- 打开弹窗前保存
document.activeElement;关闭时恢复 - 弹窗容器设
tabIndex={-1}并在挂载时.focus() - 弹窗内陷阱焦点(使用
dialog元素或 focus-trap 库)
检查清单
- 所有交互元素可通过键盘到达
- 表单输入有标签(
<label htmlFor>或aria-label) - 错误消息使用
role="alert" - 路由切换和弹窗开关时管理焦点
- 组件测试中运行
axe无障碍检查
11. 错误处理
Error Boundary(类组件,推荐 react-error-boundary 获得 Hook 友好封装)
import { ErrorBoundary } from 'react-error-boundary'
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role="alert">
<p>出错了:{error.message}</p>
<button onClick={resetErrorBoundary}>重试</button>
</div>
)
}
<ErrorBoundary FallbackComponent={ErrorFallback}>
<App />
</ErrorBoundary>
异步错误处理
async function fetchData(url: string) {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
}
12. 测试
AAA 结构
test('无匹配项时返回空数组', () => {
// Arrange(准备)
const items = [{ name: 'apple' }, { name: 'banana' }]
// Act(执行)
const result = search(items, 'cherry')
// Assert(断言)
expect(result).toEqual([])
})
组件测试(Testing Library)
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
test('提交有效数据', async () => {
const onSubmit = vi.fn()
render(<CreateForm onSubmit={onSubmit} />)
await userEvent.type(screen.getByLabelText('名称'), '测试市场')
await userEvent.click(screen.getByRole('button', { name: '提交' }))
expect(onSubmit).toHaveBeenCalledWith({ name: '测试市场' })
})
关键原则
- 测试行为,而非实现细节
- 按角色/标签查询(无障碍查询),而非 test ID
- 用
userEvent代替fireEvent实现真实交互 - 用 MSW 模拟网络,而非
fetchmock - 组件测试中运行
axe无障碍检查
13. React 19 特性
⚠️ 版本门控:本节所有特性需要 React 19+。React 18 或更早版本的项目禁止使用。每个小节注明了 React 18 替代方案。
useOptimistic — 乐观 UI
React 18 替代方案:用 useState 手动管理乐观状态 + 错误时回滚。
"use client"
import { useOptimistic } from "react"
export function MessageList({ messages }: { messages: Message[] }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newMsg: Message) => [...state, newMsg],
)
async function send(formData: FormData) {
const text = String(formData.get("text"))
addOptimistic({ id: "pending", text, sender: "me" })
await saveMessage(text)
}
return (
<>
<ul>{optimistic.map(m => <li key={m.id}>{m.text}</li>)}</ul>
<form action={send}>
<input name="text" />
<button type="submit">发送</button>
</form>
</>
)
}
useActionState — 表单状态机
替代 React 18 的 useFormState(来自 react-dom)。一个 Hook 处理 pending 状态和 Server Action 结果。
React 18 替代方案:useFormState(需 Next.js 14+),或手动 useState + isPending 模式。
use — 渲染中读取资源
React 18 替代方案:将 Promise 包装为 Suspense 兼容的 resource,或使用 TanStack Query 的 useSuspenseQuery。
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise)
return <h1>{user.name}</h1>
}
反模式检查清单
| 反模式 | 修复方案 |
|--------|----------|
| 使用超出项目 React 版本的特性 | 先检查 package.json,使用版本适配的替代方案 |
| 修改静态资源(图片、字体、public/) | 禁止;需要时向用户确认 |
| useEffect 同步派生状态 | 渲染时直接计算 |
| useEffect + fetch 获取应用数据 | TanStack Query / SWR / RSC(版本允许时) |
| Prop drilling 超过 4 层 | Context 或组合(children) |
| any 类型 | unknown + 类型收窄 |
| 数组索引作为 key | 使用数据中的稳定 ID |
| 三元嵌套 3 层以上 | 提前返回或 && 守卫 |
| 函数超过 50 行 | 拆分为更小的函数 |
| 魔法数字 | 命名常量 |
| 直接修改 state | 展开运算符 |
| 到处 React.memo | 仅在性能分析证明有效时使用 |
| Pages Router 中使用 RSC 模式 | 检查路由类型;改用 getServerSideProps/getStaticProps |
Scan to join WeChat group