react-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact 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-heavy codeuseEffect - 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 adds a render cycle, can desync, and obscures the data flow.
useEffecttsx
// 推荐:在渲染过程中推导数据
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>;
}在 中处理推导状态会增加渲染周期,可能导致数据不同步,还会模糊数据流。
useEffect2. Side Effects Outside Render
2. 副作用置于渲染之外
Effects, mutations, network calls, and subscriptions live in event handlers or — never in the render body.
useEffect副作用、数据变更、网络请求和订阅应放在事件处理函数或 中——绝不能放在渲染函数体内。
useEffect3. Composition Over Inheritance
3. 组合优于继承
React has no inheritance model for components. Compose with , render props, or component props.
childrenReact 没有组件继承模型,应通过 、渲染属性或组件属性实现组合。
childrenHooks Discipline
Hooks 规范
See rules/react/hooks.md for the full ruleset. Highlights:
- Top-level only, never conditional
- Cleanup every subscription, interval, listener
- Functional updater () when new state depends on old
setX(prev => prev + 1) - Default position: do not memoize — add /
useMemoonly when a profiler or a dependency chain proves it mattersuseCallback - Extract a custom hook only when the same hook sequence appears in 2+ components
完整规则集参见 rules/react/hooks.md。重点内容:
- 仅在顶层调用,绝不放在条件语句中
- 为所有订阅、定时器、监听器添加清理逻辑
- 当新状态依赖旧状态时,使用函数式更新()
setX(prev => prev + 1) - 默认原则:不要盲目 memoize——只有当性能分析工具或依赖链证明有必要时,再添加 /
useMemouseCallback - 仅当相同的 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 or imperatively from event handlers
<form action={...}> - Never a Server Component from a Client Component file — compose them via
importinsteadchildren
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 - 客户端 -> 服务端:通过 或在事件处理函数中调用 Server Actions
<form action={...}> - 绝不能从 Client Component 文件中 Server Component——应通过
import实现组合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 for a hook-friendly wrapper
react-error-boundary - 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;可使用 获得支持 hooks 的封装
react-error-boundary - 边界会捕获其子组件在渲染、生命周期和构造函数中抛出的错误——但不包括事件处理函数或异步代码中的错误
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
数据获取决策矩阵
| Need | Tool |
|---|---|
| Per-request data in Next.js App Router | RSC |
| Client-side cache + mutations + invalidation | TanStack Query |
| Lightweight client cache + revalidation | SWR |
| Real-time subscriptions | Server-Sent Events, WebSockets, or the lib's subscription API |
| One-off fire-and-forget | |
Avoid + for application data — race conditions, no cache, no retry, no Suspense integration.
useEffectfetch| 需求 | 工具 |
|---|---|
| Next.js App Router 中按请求获取数据 | RSC |
| 客户端缓存 + 数据变更 + 失效机制 | TanStack Query |
| 轻量客户端缓存 + 重新验证 | SWR |
| 实时订阅 | Server-Sent Events、WebSockets 或对应库的订阅 API |
| 一次性请求(无需等待结果) | 事件处理函数中的 |
避免使用 + 获取业务数据——存在竞态条件、无缓存、无重试机制、不支持 Suspense。
useEffectfetchComposition Recipes
组件组合方案
Slot via children
children基于 children
的插槽
childrentsx
<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 () returning the same shape — usually cleaner.
useData(id)当父组件需要向渲染输出传递参数时非常有用:
tsx
<DataLoader id={id}>
{({ data, isLoading }) => isLoading ? <Spinner /> : <UserCard user={data} />}
</DataLoader>现代替代方案:返回相同结构的 hook()——通常更简洁。
useData(id)Performance
性能优化
When React.memo
Actually Helps
React.memoReact.memo
的实际适用场景
React.memoWrap a component in only when:
React.memo- It re-renders frequently
- Its props are usually the same between renders
- Its render is measurably expensive
React.memo仅在以下条件都满足时,才用 包裹组件:
React.memo- 组件频繁重新渲染
- 渲染之间 props 通常保持不变
- 渲染过程存在可测量的性能开销
React.memoAvoiding Render Cascades
避免渲染级联
- Lift state down rather than up where possible
- Split context: one context per concern, so a change to does not re-render auth consumers
themeContext - Use for external state libraries — required for safe concurrent rendering
useSyncExternalStore
- 尽可能将状态向下提升而非向上
- 拆分 Context:每个 Context 对应一个关注点,这样 的变更不会导致权限相关组件重新渲染
themeContext - 对外部状态库使用 ——这是实现安全并发渲染的必需条件
useSyncExternalStore
Lists
列表优化
- Provide stable props (database id, not array index)
key - Virtualize long lists with or
@tanstack/react-virtualonce visible item count exceeds ~50 with non-trivial rowsreact-window
- 提供稳定的 props(使用数据库 ID,而非数组索引)
key - 当可见项数量超过约 50 且行内容非简单结构时,使用 或
@tanstack/react-virtual实现长列表虚拟化react-window
Accessibility-First Composition
无障碍优先的组件组合
- Always render semantic HTML (,
<button>,<a>,<nav>) before reaching for<main>attributesrole - Every interactive element must be reachable by keyboard
- Form inputs need labels — or
<label htmlFor>if visually labeled by an iconaria-label - Manage focus on route changes and modal open/close
- Run in component tests (see skills/react-testing)
axe - Cross-link: skills/accessibility/SKILL.md covers WCAG criteria and pattern libraries
- 优先使用语义化 HTML(、
<button>、<a>、<nav>),再考虑使用<main>属性role - 所有交互元素必须可通过键盘访问
- 表单输入项需要标签——使用 ,如果通过图标可视化标注则使用
<label htmlFor>aria-label - 在路由切换和模态框打开/关闭时管理焦点
- 在组件测试中运行 (参见 skills/react-testing)
axe - 关联参考: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 skill (not present yet)
react-native-patterns - 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: for code review,
react-reviewerfor build/bundler errorsreact-build-resolver - Commands: ,
/react-review,/react-build/react-test
- 规则:rules/react/ —— 编码风格、hooks、模式、安全、测试
- 技能:react-performance(基于 Vercel 的性能规则集)、frontend-patterns(跨框架 UI 关注点)、accessibility、angular-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
useOptimistic基于 React 19 useOptimistic
的乐观 UI
useOptimistictsx
"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 changetsx
// 两个独立 Context:一个极少变更,一个频繁变更
const ThemeContext = createContext<Theme>("light");
const NotificationsContext = createContext<Notification[]>([]);
// 仅消费 ThemeContext 的组件不会在通知变更时重新渲染