Back to skills
extension
Category: Development & EngineeringNo API key required

React增强版

React 专业领域技能(中文版)— 涵盖 TypeScript 规范、组件组合、Hooks 纪律、状态架构、 RSC 边界、数据获取、表单、性能优化、无障碍、错误处理和测试。

personAuthor: deankwanhubModelScope

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 或自定义流式渲染。

规则

  1. 禁止使用超出项目 React 版本的特性。 项目为 React 17 时,不得使用 useTransition、Suspense 数据获取或 RSC。
  2. 同时检查 next 版本 — RSC 模式需要 Next.js 13+ App Router;Pages Router 项目不能使用。
  3. 不确定时,先询问。 不要假设项目已升级。

静态资源 — 禁止修改

以下文件视为不可变,绝不能修改:

  • 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.jsonyarn.lockpnpm-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 文件中 import Server 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 何时有效

仅当以下三个条件同时满足时才包裹:

  1. 频繁重渲染
  2. props 在渲染间通常相同
  3. 渲染开销可被度量

否则相等性检查本身就是额外开销。

记忆化

// 昂贵计算
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 模拟网络,而非 fetch mock
  • 组件测试中运行 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 |