react-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Patterns

React 模式

Idiomatic React 18/19 patterns for building robust, accessible, performant component trees.
用于构建健壮、无障碍、高性能组件树的 React 18/19 惯用模式。

When to Activate

适用场景

  • Writing or modifying React function components, custom hooks, or component trees
  • Reviewing JSX/TSX files
  • Designing state shape or component composition
  • Migrating class components or older
    forwardRef
    /
    useEffect
    -heavy code
  • Choosing between local state, lifted state, context, and external stores
  • Working with Server Components / Client Components (Next.js App Router, RSC)
  • Implementing forms with React 19 actions or controlled inputs
  • Wiring data fetching with TanStack Query / SWR / RSC
  • 编写或修改 React 函数组件、自定义 hooks 或组件树
  • 评审 JSX/TSX 文件
  • 设计状态结构或组件组合方式
  • 迁移类组件或依赖
    forwardRef
    /
    useEffect
    较多的旧代码
  • 在局部状态、提升状态、Context 和外部状态库之间做选择
  • 使用 Server Components / Client Components(Next.js App Router、RSC)开发
  • 基于 React 18/19 表单动作或受控输入实现表单
  • 结合 TanStack Query / SWR / RSC 实现数据获取

Core Principles

核心原则

1. Render is a Pure Function of Props and State

1. 渲染是 Props 和 State 的纯函数

tsx
// Good: derive during render
function Cart({ items }: { items: CartItem[] }) {
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  return <span>{formatMoney(total)}</span>;
}

// Bad: derived state stored separately
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>;
}
Derived state in
useEffect
adds a render cycle, can desync, and obscures the data flow.
tsx
// 推荐:在渲染过程中推导数据
function Cart({ items }: { items: CartItem[] }) {
  const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
  return <span>{formatMoney(total)}</span>;
}

// 不推荐:单独存储推导状态
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>;
}
useEffect
中处理推导状态会增加渲染周期,可能导致数据不同步,还会模糊数据流。

2. Side Effects Outside Render

2. 副作用置于渲染之外

Effects, mutations, network calls, and subscriptions live in event handlers or
useEffect
— never in the render body.
副作用、数据变更、网络请求和订阅应放在事件处理函数或
useEffect
中——绝不能放在渲染函数体内。

3. Composition Over Inheritance

3. 组合优于继承

React has no inheritance model for components. Compose with
children
, render props, or component props.
React 没有组件继承模型,应通过
children
、渲染属性或组件属性实现组合。

Hooks Discipline

Hooks 规范

See rules/react/hooks.md for the full ruleset. Highlights:
  • Top-level only, never conditional
  • Cleanup every subscription, interval, listener
  • Functional updater (
    setX(prev => prev + 1)
    ) when new state depends on old
  • Default position: do not memoize — add
    useMemo
    /
    useCallback
    only when a profiler or a dependency chain proves it matters
  • Extract a custom hook only when the same hook sequence appears in 2+ components
完整规则集参见 rules/react/hooks.md。重点内容:
  • 仅在顶层调用,绝不放在条件语句中
  • 为所有订阅、定时器、监听器添加清理逻辑
  • 当新状态依赖旧状态时,使用函数式更新(
    setX(prev => prev + 1)
  • 默认原则:不要盲目 memoize——只有当性能分析工具或依赖链证明有必要时,再添加
    useMemo
    /
    useCallback
  • 仅当相同的 hooks 序列出现在 2 个及以上组件中时,才提取为自定义 hook

State Location Decision Tree

状态位置决策树

Used by one component?
  -> useState inside it

Used by parent + a few descendants?
  -> lift to nearest common ancestor

Used across distant branches AND low-frequency reads (theme, auth, locale)?
  -> React Context

High-frequency updates shared across the tree?
  -> external store (Zustand, Jotai, Redux Toolkit)

Derived from a server?
  -> server-state library (TanStack Query, SWR, RSC fetch)
Most pages do not need context or a global store. Resist abstraction until duplicated lifting becomes painful.
仅单个组件使用?
  -> 在组件内部使用 useState

父组件和少数后代组件使用?
  -> 提升到最近的共同祖先组件

跨多个分支使用且读取频率低(主题、权限、语言)?
  -> 使用 React Context

树中多个组件共享且更新频率高?
  -> 使用外部状态库(Zustand、Jotai、Redux Toolkit)

数据来自服务端?
  -> 使用服务端状态库(TanStack Query、SWR、RSC fetch)
大多数页面不需要 Context 或全局状态库。在重复提升状态变得繁琐之前,避免过度抽象。

Server / Client Components (RSC)

服务端/客户端组件(RSC)

tsx
// Server Component - default, async, never ships JS for itself
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 - opt in with "use client"
"use client";
export function AddToCartButton({ productId }: { productId: string }) {
  const [pending, startTransition] = useTransition();
  return (
    <button
      disabled={pending}
      onClick={() => startTransition(() => addToCart(productId))}
    >
      {pending ? "Adding..." : "Add to cart"}
    </button>
  );
}
Boundaries:
  • Server -> Client: pass serializable props or
    children
  • Client -> Server: invoke Server Actions via
    <form action={...}>
    or imperatively from event handlers
  • Never
    import
    a Server Component from a Client Component file — compose them via
    children
    instead
tsx
// Server Component - 默认类型,支持异步,自身不会打包发送 JS
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" 声明启用
"use client";
export function AddToCartButton({ productId }: { productId: string }) {
  const [pending, startTransition] = useTransition();
  return (
    <button
      disabled={pending}
      onClick={() => startTransition(() => addToCart(productId))}
    >
      {pending ? "Adding..." : "Add to cart"}
    </button>
  );
}
边界规则:
  • 服务端 -> 客户端:传递可序列化的 props 或
    children
  • 客户端 -> 服务端:通过
    <form action={...}>
    或在事件处理函数中调用 Server Actions
  • 绝不能从 Client Component 文件中
    import
    Server Component——应通过
    children
    实现组合

Suspense + Error Boundaries

Suspense + 错误边界

tsx
<ErrorBoundary fallback={<ErrorView />}>
  <Suspense fallback={<UserSkeleton />}>
    <UserDetail id={id} />
  </Suspense>
</ErrorBoundary>
  • Place Suspense boundaries close to the data, not at the route root — progressively reveal content
  • Error Boundary remains a class API; use
    react-error-boundary
    for a hook-friendly wrapper
  • A boundary catches errors thrown during render, lifecycle, and constructors of its children — NOT in event handlers or async code
tsx
<ErrorBoundary fallback={<ErrorView />}>
  <Suspense fallback={<UserSkeleton />}>
    <UserDetail id={id} />
  </Suspense>
</ErrorBoundary>
  • 将 Suspense 边界放在靠近数据的位置,而非路由根节点——实现渐进式内容展示
  • 错误边界仍是类组件 API;可使用
    react-error-boundary
    获得支持 hooks 的封装
  • 边界会捕获其子组件在渲染、生命周期和构造函数中抛出的错误——但不包括事件处理函数或异步代码中的错误

Forms

表单

React 19 form actions (preferred for new code)

React 19 表单动作(新项目优先使用)

tsx
"use client";
import { useActionState } from "react";

const initial = { error: null as string | null };

async function updateUserAction(_prev: typeof initial, formData: FormData) {
  "use server";
  const parsed = UserSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: "Invalid input" };
  await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
  return { error: null };
}

export function UserForm() {
  const [state, formAction, pending] = useActionState(updateUserAction, initial);
  return (
    <form action={formAction}>
      <input name="name" required />
      <button type="submit" disabled={pending}>Save</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}
tsx
"use client";
import { useActionState } from "react";

const initial = { error: null as string | null };

async function updateUserAction(_prev: typeof initial, formData: FormData) {
  "use server";
  const parsed = UserSchema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) return { error: "Invalid input" };
  await db.user.update({ where: { id: parsed.data.id }, data: parsed.data });
  return { error: null };
}

export function UserForm() {
  const [state, formAction, pending] = useActionState(updateUserAction, initial);
  return (
    <form action={formAction}>
      <input name="name" required />
      <button type="submit" disabled={pending}>Save</button>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

Controlled inputs

受控输入

Use controlled when the value drives other UI, formats on every keystroke, or implements real-time validation.
当输入值驱动其他 UI、需随每次按键格式化或实现实时验证时,使用受控输入。

Complex forms

复杂表单

For multi-step forms, dynamic field arrays, or cross-field validation: use a library (React Hook Form, TanStack Form). Roll-your-own state management for forms past trivial complexity is a maintenance trap.
对于多步骤表单、动态字段数组或跨字段验证:使用表单库(React Hook Form、TanStack Form)。超出简单复杂度后,自行实现表单状态管理会成为维护陷阱。

Data Fetching Decision Matrix

数据获取决策矩阵

NeedTool
Per-request data in Next.js App RouterRSC
await fetch()
Client-side cache + mutations + invalidationTanStack Query
Lightweight client cache + revalidationSWR
Real-time subscriptionsServer-Sent Events, WebSockets, or the lib's subscription API
One-off fire-and-forget
fetch()
in an event handler
Avoid
useEffect
+
fetch
for application data — race conditions, no cache, no retry, no Suspense integration.
需求工具
Next.js App Router 中按请求获取数据RSC
await fetch()
客户端缓存 + 数据变更 + 失效机制TanStack Query
轻量客户端缓存 + 重新验证SWR
实时订阅Server-Sent Events、WebSockets 或对应库的订阅 API
一次性请求(无需等待结果)事件处理函数中的
fetch()
避免使用
useEffect
+
fetch
获取业务数据——存在竞态条件、无缓存、无重试机制、不支持 Suspense。

Composition Recipes

组件组合方案

Slot via
children

基于
children
的插槽

tsx
<Layout>
  <Header />
  <Main>{content}</Main>
</Layout>
tsx
<Layout>
  <Header />
  <Main>{content}</Main>
</Layout>

Named slots

命名插槽

tsx
<Page header={<Nav />} sidebar={<Filters />}>
  <Results />
</Page>
tsx
<Page header={<Nav />} sidebar={<Filters />}>
  <Results />
</Page>

Compound components (shared state via Context)

复合组件(通过 Context 共享状态)

tsx
<Tabs defaultValue="profile">
  <Tabs.List>
    <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="profile"><Profile /></Tabs.Panel>
  <Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>
tsx
<Tabs defaultValue="profile">
  <Tabs.List>
    <Tabs.Trigger value="profile">Profile</Tabs.Trigger>
    <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
  </Tabs.List>
  <Tabs.Panel value="profile"><Profile /></Tabs.Panel>
  <Tabs.Panel value="settings"><Settings /></Tabs.Panel>
</Tabs>

Render prop / function-as-child

渲染属性/函数作为子组件

Useful when the parent needs to pass parameters to the rendered output:
tsx
<DataLoader id={id}>
  {({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
Modern alternative: a hook (
useData(id)
) returning the same shape — usually cleaner.
当父组件需要向渲染输出传递参数时非常有用:
tsx
<DataLoader id={id}>
  {({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>
现代替代方案:返回相同结构的 hook(
useData(id)
)——通常更简洁。

Performance

性能优化

When
React.memo
Actually Helps

React.memo
的实际适用场景

Wrap a component in
React.memo
only when:
  1. It re-renders frequently
  2. Its props are usually the same between renders
  3. Its render is measurably expensive
React.memo
adds an equality check on every render. If props differ on most renders, the check is pure overhead.
仅在以下条件都满足时,才用
React.memo
包裹组件:
  1. 组件频繁重新渲染
  2. 渲染之间 props 通常保持不变
  3. 渲染过程存在可测量的性能开销
React.memo
会在每次渲染时添加相等性检查。如果大多数渲染中 props 都不同,这个检查纯粹是额外开销。

Avoiding Render Cascades

避免渲染级联

  • Lift state down rather than up where possible
  • Split context: one context per concern, so a change to
    themeContext
    does not re-render auth consumers
  • Use
    useSyncExternalStore
    for external state libraries — required for safe concurrent rendering
  • 尽可能将状态向下提升而非向上
  • 拆分 Context:每个 Context 对应一个关注点,这样
    themeContext
    的变更不会导致权限相关组件重新渲染
  • 对外部状态库使用
    useSyncExternalStore
    ——这是实现安全并发渲染的必需条件

Lists

列表优化

  • Provide stable
    key
    props (database id, not array index)
  • Virtualize long lists with
    @tanstack/react-virtual
    or
    react-window
    once visible item count exceeds ~50 with non-trivial rows
  • 提供稳定的
    key
    props(使用数据库 ID,而非数组索引)
  • 当可见项数量超过约 50 且行内容非简单结构时,使用
    @tanstack/react-virtual
    react-window
    实现长列表虚拟化

Accessibility-First Composition

无障碍优先的组件组合

  • Always render semantic HTML (
    <button>
    ,
    <a>
    ,
    <nav>
    ,
    <main>
    ) before reaching for
    role
    attributes
  • Every interactive element must be reachable by keyboard
  • Form inputs need labels —
    <label htmlFor>
    or
    aria-label
    if visually labeled by an icon
  • Manage focus on route changes and modal open/close
  • Run
    axe
    in component tests (see skills/react-testing)
  • Cross-link: skills/accessibility/SKILL.md covers WCAG criteria and pattern libraries
  • 优先使用语义化 HTML(
    <button>
    <a>
    <nav>
    <main>
    ),再考虑使用
    role
    属性
  • 所有交互元素必须可通过键盘访问
  • 表单输入项需要标签——使用
    <label htmlFor>
    ,如果通过图标可视化标注则使用
    aria-label
  • 在路由切换和模态框打开/关闭时管理焦点
  • 在组件测试中运行
    axe
    (参见 skills/react-testing
  • 关联参考:skills/accessibility/SKILL.md 涵盖 WCAG 标准和模式库

Routing

路由

This skill is router-agnostic. The patterns above work with React Router, TanStack Router, Next.js App Router, Remix Router. Router-specific patterns (loaders, actions, nested layouts) follow the router's documentation — those are framework concerns layered on top of React core.
本指南与路由库无关。上述模式适用于 React Router、TanStack Router、Next.js App Router、Remix Router。路由特定模式(加载器、动作、嵌套布局)请遵循对应路由库的文档——这些是基于 React 核心之上的框架相关内容。

Out of Scope (Pointer Sections)

超出范围的内容

  • Next.js specifics: App Router data loading, Route Handlers, Middleware, Parallel Routes — separate concern, use Next.js docs
  • React Native: Platform-specific patterns differ enough to warrant a separate
    react-native-patterns
    skill (not present yet)
  • Remix: Loader/action conventions overlap with RSC but follow Remix docs
  • Next.js 特定功能:App Router 数据加载、Route Handlers、Middleware、Parallel Routes——属于独立关注点,请参考 Next.js 文档
  • React Native:平台特定模式差异较大,需要单独的
    react-native-patterns
    指南(目前尚未提供)
  • Remix:Loader/action 约定与 RSC 有重叠,请遵循 Remix 文档

Related

相关资源

  • Rules: rules/react/ — coding-style, hooks, patterns, security, testing
  • Skills: react-performance for the Vercel-derived performance ruleset, frontend-patterns for cross-framework UI concerns, accessibility, angular-developer for framework comparison
  • Agents:
    react-reviewer
    for code review,
    react-build-resolver
    for build/bundler errors
  • Commands:
    /react-review
    ,
    /react-build
    ,
    /react-test
  • 规则:rules/react/ —— 编码风格、hooks、模式、安全、测试
  • 技能:react-performance(基于 Vercel 的性能规则集)、frontend-patterns(跨框架 UI 关注点)、accessibilityangular-developer(框架对比)
  • Agents:
    react-reviewer
    (代码评审)、
    react-build-resolver
    (构建/打包错误排查)
  • 命令:
    /react-review
    /react-build
    /react-test

Examples

示例

Custom hook for debounced search

用于防抖搜索的自定义 hook

tsx
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 SearchBox() {
  const [query, setQuery] = useState("");
  const debounced = useDebounce(query, 300);
  const { data } = useQuery({
    queryKey: ["search", debounced],
    queryFn: () => searchApi(debounced),
    enabled: debounced.length > 0,
  });
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results items={data ?? []} />
    </>
  );
}
tsx
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 SearchBox() {
  const [query, setQuery] = useState("");
  const debounced = useDebounce(query, 300);
  const { data } = useQuery({
    queryKey: ["search", debounced],
    queryFn: () => searchApi(debounced),
    enabled: debounced.length > 0,
  });
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <Results items={data ?? []} />
    </>
  );
}

Optimistic UI with React 19
useOptimistic

基于 React 19
useOptimistic
的乐观 UI

tsx
"use client";
import { useOptimistic } from "react";

export function MessageList({ messages }: { messages: Message[] }) {
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    (state, newMessage: Message) => [...state, newMessage],
  );

  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">Send</button>
      </form>
    </>
  );
}
tsx
"use client";
import { useOptimistic } from "react";

export function MessageList({ messages }: { messages: Message[] }) {
  const [optimistic, addOptimistic] = useOptimistic(
    messages,
    (state, newMessage: Message) => [...state, newMessage],
  );

  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">Send</button>
      </form>
    </>
  );
}

Splitting context to avoid render cascades

拆分 Context 避免渲染级联

tsx
// Two contexts: one rarely changes, one frequently
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);

// A component that only consumes ThemeContext does NOT re-render when notifications change
tsx
// 两个独立 Context:一个极少变更,一个频繁变更
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);

// 仅消费 ThemeContext 的组件不会在通知变更时重新渲染