react-best-practices

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Best Practices

React 最佳实践

Preconditions

前置条件

Before applying these practices, confirm:
  1. Stack check - Verify React Router 7 is in use (or note if using different router/framework)
  2. TypeScript - Confirm TypeScript is configured with
    strict: true
  3. Existing patterns - Review existing codebase patterns for consistency
If the codebase uses a different data fetching approach (TanStack Query, SWR, etc.), adapt the data fetching guidance accordingly.
在应用这些实践之前,请确认:
  1. 技术栈检查 - 确认正在使用React Router 7(若使用其他路由/框架请注明)
  2. TypeScript配置 - 确认TypeScript已配置
    strict: true
  3. 现有模式对齐 - 评审现有代码库的模式以保持一致性
如果代码库使用其他数据获取方案(TanStack Query、SWR等),请相应调整数据获取指导方案。

Steps

实施步骤

When writing or reviewing React code:
  1. Audit useEffect usage - For each useEffect, ask "Can this be derived state, an event handler, or handled by the router?"
  2. Choose state placement - Follow the hierarchy: component → URL → lifted → context
  3. Verify data fetching - Ensure loaders/actions (or client cache) handle fetching, not raw useEffect
  4. Check component design - Apply composition patterns, verify single responsibility
  5. Validate keys - Ensure list keys are stable and unique (not index or random)
  6. Review TypeScript - Props have explicit interfaces, no
    any
    types
  7. Check accessibility - Semantic HTML, focus management, keyboard support
  8. Profile if needed - Only add memoization after measuring; consider
    useTransition
    /
    useDeferredValue
    first
在编写或评审React代码时:
  1. 审计useEffect使用 - 对每个useEffect,思考“这是否可以通过派生状态、事件处理函数或路由来实现?”
  2. 选择状态存放位置 - 遵循层级顺序:组件内部 → URL → 提升至父组件 → Context
  3. 验证数据获取方式 - 确保数据获取由loader/action(或客户端缓存)处理,而非直接使用useEffect
  4. 检查组件设计 - 应用组合模式,验证单一职责原则
  5. 验证key属性 - 确保列表key稳定且唯一(避免使用索引或随机值)
  6. 评审TypeScript代码 - Props有明确的接口定义,禁止使用
    any
    类型
  7. 检查可访问性 - 使用语义化HTML、焦点管理、键盘支持
  8. 按需性能分析 - 仅在测量后添加 memoization;优先考虑
    useTransition
    /
    useDeferredValue

Examples

示例

Example 1: Refactor effect-heavy component

示例1:重构重度依赖effect的组件

User prompt: "This component has too many useEffects, can you clean it up?"
Agent behavior:
  1. Identify each useEffect and categorize: derived state? event response? data fetch? external sync?
  2. Convert derived state to render-time calculations
  3. Move event responses into event handlers
  4. Replace data fetching with loader or client cache
  5. Keep only effects that sync with external systems
  6. Verify cleanup functions exist where needed
用户需求:“这个组件有太多useEffect,能帮忙清理一下吗?”
Agent操作
  1. 识别每个useEffect并分类:派生状态?事件响应?数据获取?外部同步?
  2. 将派生状态转换为渲染时计算
  3. 将事件响应移至事件处理函数中
  4. 用loader或客户端缓存替换数据获取逻辑
  5. 仅保留用于与外部系统同步的effect
  6. 确认必要的清理函数已存在

Example 2: Convert fetch-in-effect to loader

示例2:将effect中的fetch转换为loader

User prompt: "Migrate this component to use React Router loaders"
Agent behavior:
  1. Identify the useEffect doing the fetch
  2. Create a loader function in the route definition
  3. Replace useState + useEffect with useLoaderData()
  4. Handle loading states via useNavigation() instead of local state
  5. Move error handling to the loader or errorElement
  6. Remove the now-unnecessary effect and state
用户需求:“将这个组件迁移为使用React Router loader”
Agent操作
  1. 识别执行fetch的useEffect
  2. 在路由定义中创建loader函数
  3. 用useLoaderData()替换useState + useEffect
  4. 通过useNavigation()处理加载状态,而非本地状态
  5. 将错误处理移至loader或errorElement
  6. 删除不再需要的effect和状态

Example 3: Fix list performance issues

示例3:修复列表性能问题

User prompt: "This list re-renders constantly and is slow"
Agent behavior:
  1. Check key prop - ensure stable IDs, not index or random
  2. Profile to identify expensive renders
  3. Wrap list item component in React.memo if pure
  4. Ensure callbacks passed to items use useCallback
  5. Check if parent state changes are causing unnecessary re-renders
  6. Consider virtualization for very long lists

用户需求:“这个列表持续重渲染,运行缓慢”
Agent操作
  1. 检查key属性 - 确保使用稳定ID,而非索引或随机值
  2. 分析性能以识别昂贵的渲染操作
  3. 如果是纯组件,将列表项组件用React.memo包裹
  4. 确保传递给子项的回调使用useCallback
  5. 检查父组件状态变化是否导致不必要的重渲染
  6. 对于超长列表,考虑使用虚拟化技术

Core Principle: Avoid useEffect

核心原则:避免使用useEffect

Most useEffect usage is unnecessary. Before reaching for useEffect, ask: "Can this be done another way?"
大多数useEffect的使用都是不必要的。在使用useEffect之前,请思考:“有没有其他实现方式?”

Do NOT Use useEffect For

以下场景请勿使用useEffect

Derived state - Calculate during render:
tsx
// BAD
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// GOOD
const fullName = `${firstName} ${lastName}`;
Event responses - Handle in event handlers:
tsx
// BAD
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
  if (submitted) {
    submitForm(data);
  }
}, [submitted, data]);

// GOOD
function handleSubmit() {
  submitForm(data);
}
Initializing state - Use useState initializer:
tsx
// BAD
const [items, setItems] = useState([]);
useEffect(() => {
  setItems(getInitialItems());
}, []);

// GOOD
const [items, setItems] = useState(() => getInitialItems());
Data fetching - Use React Router loaders (see below).
派生状态 - 在渲染时计算:
tsx
// 不良实践
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

// 最佳实践
const fullName = `${firstName} ${lastName}`;
事件响应 - 在事件处理函数中处理:
tsx
// 不良实践
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
  if (submitted) {
    submitForm(data);
  }
}, [submitted, data]);

// 最佳实践
function handleSubmit() {
  submitForm(data);
}
初始化状态 - 使用useState初始化函数:
tsx
// 不良实践
const [items, setItems] = useState([]);
useEffect(() => {
  setItems(getInitialItems());
}, []);

// 最佳实践
const [items, setItems] = useState(() => getInitialItems());
数据获取 - 使用React Router loader(见下文)。

When useEffect IS Appropriate

以下场景适合使用useEffect

  • Subscribing to external systems (WebSocket, browser APIs)
  • Third-party library integration (charts, maps, video players)
  • Event listeners that need cleanup
  • Synchronizing with non-React code
When you must use useEffect:
tsx
useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect(); // Always clean up
}, [roomId]);
  • 订阅外部系统(WebSocket、浏览器API)
  • 第三方库集成(图表、地图、视频播放器)
  • 需要清理的事件监听器
  • 与非React代码同步
当必须使用useEffect时:
tsx
useEffect(() => {
  const connection = createConnection(roomId);
  connection.connect();
  return () => connection.disconnect(); // 始终要清理
}, [roomId]);

Hooks Hygiene

Hooks 规范

Dependency Arrays

依赖数组

Never disable
exhaustive-deps
without a very good reason. If you think you need to:
  1. The effect probably shouldn't be an effect
  2. You may need useCallback/useMemo for stable references
  3. Consider useRef for values that shouldn't trigger re-runs
tsx
// BAD - suppressing the linter
useEffect(() => {
  doSomething(value);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Missing 'value'

// GOOD - fix the actual issue
const stableCallback = useCallback(() => doSomething(value), [value]);
useEffect(() => {
  stableCallback();
}, [stableCallback]);
永远不要禁用
exhaustive-deps
,除非有非常充分的理由。如果你认为需要禁用:
  1. 这个逻辑可能根本不应该用effect实现
  2. 你可能需要用useCallback/useMemo来创建稳定引用
  3. 考虑使用useRef存储不应该触发重渲染的值
tsx
// 不良实践 - 抑制lint警告
useEffect(() => {
  doSomething(value);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 缺少'value'

// 最佳实践 - 修复实际问题
const stableCallback = useCallback(() => doSomething(value), [value]);
useEffect(() => {
  stableCallback();
}, [stableCallback]);

StrictMode Double Invocation

StrictMode 双重调用

In development, React StrictMode intentionally double-invokes effects to help find bugs. Your effects should handle this:
  • Effects run setup → cleanup → setup
  • If this breaks something, your effect has a bug (usually missing cleanup)
  • This helps catch issues before production
在开发环境中,React StrictMode会故意双重调用effect以帮助发现bug。你的effect应该能处理这种情况:
  • Effect执行流程:初始化 → 清理 → 初始化
  • 如果这导致问题,说明你的effect存在bug(通常是缺少清理逻辑)
  • 这有助于在生产环境前发现问题

useLayoutEffect

useLayoutEffect

Use
useLayoutEffect
only when you need to measure DOM or prevent visual flicker:
tsx
// useLayoutEffect - runs synchronously after DOM mutations
useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setPosition({ top: rect.top, left: rect.left });
}, []);

// useEffect - runs after paint (preferred for most cases)
useEffect(() => {
  trackPageView();
}, []);
Prefer
useEffect
unless you see visual flicker that
useLayoutEffect
would fix.
仅当需要测量DOM或避免视觉闪烁时才使用
useLayoutEffect
tsx
// useLayoutEffect - 在DOM变更后同步执行
useLayoutEffect(() => {
  const rect = ref.current.getBoundingClientRect();
  setPosition({ top: rect.top, left: rect.left });
}, []);

// useEffect - 在绘制后执行(大多数场景优先使用)
useEffect(() => {
  trackPageView();
}, []);
优先使用
useEffect
,除非你遇到
useLayoutEffect
可以解决的视觉闪烁问题。

Data Fetching with React Router 7

使用React Router 7进行数据获取

Prefer framework-level data fetching over useEffect. Use React Router's loaders and actions.
If not using React Router loaders, use a client cache library (TanStack Query, SWR) which handles:
  • Request deduplication
  • Caching and revalidation
  • Race condition prevention
  • Loading/error states
If you must fetch in useEffect (rare), handle cleanup and race conditions:
tsx
useEffect(() => {
  let cancelled = false;
  const controller = new AbortController();

  async function fetchData() {
    try {
      const res = await fetch("/api/data", { signal: controller.signal });
      if (!cancelled) setData(await res.json());
    } catch (e) {
      if (!cancelled && e.name !== "AbortError") setError(e);
    }
  }
  fetchData();

  return () => {
    cancelled = true;
    controller.abort();
  };
}, []);
优先使用框架级的数据获取方案,而非useEffect。使用React Router的loader和action。
如果不使用React Router loader,请使用客户端缓存库(TanStack Query、SWR),它们可以处理:
  • 请求去重
  • 缓存和重新验证
  • 竞态条件预防
  • 加载/错误状态
如果必须在useEffect中获取数据(罕见情况),请处理清理和竞态条件:
tsx
useEffect(() => {
  let cancelled = false;
  const controller = new AbortController();

  async function fetchData() {
    try {
      const res = await fetch("/api/data", { signal: controller.signal });
      if (!cancelled) setData(await res.json());
    } catch (e) {
      if (!cancelled && e.name !== "AbortError") setError(e);
    }
  }
  fetchData();

  return () => {
    cancelled = true;
    controller.abort();
  };
}, []);

Loaders for Reading Data

使用Loader读取数据

tsx
// In route definition
{
  path: "posts",
  element: <Posts />,
  loader: async () => {
    const posts = await fetch("/api/posts").then(r => r.json());
    return { posts };
  }
}

// In component
function Posts() {
  const { posts } = useLoaderData();
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
tsx
// 在路由定义中
{
  path: "posts",
  element: <Posts />,
  loader: async () => {
    const posts = await fetch("/api/posts").then(r => r.json());
    return { posts };
  }
}

// 在组件中
function Posts() {
  const { posts } = useLoaderData();
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

Actions for Mutations

使用Actions处理变更

tsx
// In route definition
{
  path: "posts/new",
  element: <NewPost />,
  action: async ({ request }) => {
    const formData = await request.formData();
    // Note: formData.get() returns FormDataEntryValue (string | File) or null
    const title = formData.get("title");
    if (typeof title !== "string") {
      return { error: "Title is required" };
    }

    const response = await fetch("/api/posts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title })
    });

    if (!response.ok) {
      return { error: "Failed to create post" };
    }

    return redirect("/posts");
  }
}

// In component - use Form, not onSubmit with fetch
function NewPost() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input name="title" required />
      <button disabled={isSubmitting}>
        {isSubmitting ? "Creating..." : "Create"}
      </button>
    </Form>
  );
}
tsx
// 在路由定义中
{
  path: "posts/new",
  element: <NewPost />,
  action: async ({ request }) => {
    const formData = await request.formData();
    // 注意:formData.get()返回FormDataEntryValue(string | File)或null
    const title = formData.get("title");
    if (typeof title !== "string") {
      return { error: "标题为必填项" };
    }

    const response = await fetch("/api/posts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title })
    });

    if (!response.ok) {
      return { error: "创建文章失败" };
    }

    return redirect("/posts");
  }
}

// 在组件中 - 使用Form,而非带fetch的onSubmit
function NewPost() {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <Form method="post">
      <input name="title" required />
      <button disabled={isSubmitting}>
        {isSubmitting ? "创建中..." : "创建"}
      </button>
    </Form>
  );
}

Key Hooks

核心Hooks

  • useLoaderData()
    - Access loader data
  • useActionData()
    - Access action return value (errors, etc.)
  • useNavigation()
    - Track navigation/submission state
  • useFetcher()
    - For mutations without navigation
  • useLoaderData()
    - 访问loader返回的数据
  • useActionData()
    - 访问action返回值(错误信息等)
  • useNavigation()
    - 跟踪导航/提交状态
  • useFetcher()
    - 用于不涉及导航的变更操作

State Management

状态管理

State Placement Hierarchy

状态存放层级

Place state as close to where it's used as possible:
  1. Component state - useState for local UI state
  2. URL state - Query params for shareable state
  3. Lifted state - Shared parent for sibling communication
  4. Context - Deeply nested access (use sparingly)
将状态存放在离使用位置尽可能近的地方:
  1. 组件状态 - 使用useState管理本地UI状态
  2. URL状态 - 使用查询参数管理可共享状态
  3. 提升状态 - 共享给父组件以实现兄弟组件通信
  4. Context - 用于深层嵌套组件的状态访问(谨慎使用)

URL State for Shareable UI

使用URL状态管理可共享UI

Use URL query params for state that should be shareable or bookmarkable:
tsx
// BAD - modal state lost on refresh/share
const [isOpen, setIsOpen] = useState(false);

// GOOD - modal state in URL
import { useSearchParams } from "react-router";

function ProductPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const isModalOpen = searchParams.get("modal") === "open";

  function openModal() {
    setSearchParams({ modal: "open" });
  }

  function closeModal() {
    setSearchParams({});
  }

  return (
    <>
      <button onClick={openModal}>View Details</button>
      {isModalOpen && <Modal onClose={closeModal} />}
    </>
  );
}
Good candidates for URL state:
  • Modal/dialog open state
  • Active tab
  • Filter/sort options
  • Pagination
  • Search queries
使用URL查询参数管理需要共享或可收藏的状态:
tsx
// 不良实践 - 刷新/分享时模态框状态丢失
const [isOpen, setIsOpen] = useState(false);

// 最佳实践 - 模态框状态存储在URL中
import { useSearchParams } from "react-router";

function ProductPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const isModalOpen = searchParams.get("modal") === "open";

  function openModal() {
    setSearchParams({ modal: "open" });
  }

  function closeModal() {
    setSearchParams({});
  }

  return (
    <>
      <button onClick={openModal}>查看详情</button>
      {isModalOpen && <Modal onClose={closeModal} />}
    </>
  );
}
适合用URL状态管理的场景:
  • 模态框/对话框的打开状态
  • 激活的标签页
  • 筛选/排序选项
  • 分页
  • 搜索查询

useState vs useReducer

useState vs useReducer

  • useState - Simple values, independent updates
  • useReducer - Complex state, related values that change together
tsx
// Good useReducer candidate - related state
const [state, dispatch] = useReducer(formReducer, {
  values: {},
  errors: {},
  touched: {},
  isSubmitting: false
});
  • useState - 简单值、独立更新
  • useReducer - 复杂状态、相关值的联动更新
tsx
// 适合使用useReducer的场景 - 关联状态
const [state, dispatch] = useReducer(formReducer, {
  values: {},
  errors: {},
  touched: {},
  isSubmitting: false
});

Context Pitfalls

Context 的陷阱

Avoid single large context - it causes unnecessary re-renders:
tsx
// BAD - all consumers re-render on any change
<AppContext.Provider value={{ user, theme, settings, cart }}>

// GOOD - separate contexts by domain
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <CartContext.Provider value={cart}>
避免使用单一大型Context - 这会导致不必要的重渲染:
tsx
// 不良实践 - 任何变更都会导致所有消费者重渲染
<AppContext.Provider value={{ user, theme, settings, cart }}>

// 最佳实践 - 按领域拆分Context
<UserContext.Provider value={user}>
  <ThemeContext.Provider value={theme}>
    <CartContext.Provider value={cart}>

Memoize Provider Values

缓存Provider的值

Always memoize context values to prevent unnecessary re-renders:
tsx
// BAD - new object every render
<ThemeContext.Provider value={{ theme, setTheme }}>

// GOOD - memoized value
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>
始终缓存Context的值以避免不必要的重渲染:
tsx
// 不良实践 - 每次渲染都会创建新对象
<ThemeContext.Provider value={{ theme, setTheme }}>

// 最佳实践 - 缓存后的值
const value = useMemo(() => ({ theme, setTheme }), [theme]);
<ThemeContext.Provider value={value}>

High-Churn State

高频更新状态

For frequently updating state (mouse position, animations), consider:
  • useSyncExternalStore
    for external state stores
  • Zustand, Jotai, or similar for fine-grained subscriptions
  • Keep high-churn state out of Context entirely
对于频繁更新的状态(鼠标位置、动画),可以考虑:
  • 使用
    useSyncExternalStore
    处理外部状态存储
  • 使用Zustand、Jotai等实现细粒度订阅
  • 完全避免将高频更新状态放入Context

Component Design

组件设计

Composition Over Configuration

组合优于配置

Build flexible components using composition, not props. Follow shadcn/ui patterns:
tsx
// BAD - configuration via props
<Dialog
  title="Edit Profile"
  description="Make changes here"
  content={<ProfileForm />}
  onConfirm={handleSave}
  onCancel={handleClose}
/>

// GOOD - composition via children
<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">Edit Profile</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Edit Profile</DialogTitle>
      <DialogDescription>Make changes here</DialogDescription>
    </DialogHeader>
    <ProfileForm />
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="outline">Cancel</Button>
      </DialogClose>
      <Button onClick={handleSave}>Save</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
使用组合而非props构建灵活的组件。遵循shadcn/ui的模式:
tsx
// 不良实践 - 通过props配置
<Dialog
  title="编辑资料"
  description="在此处进行修改"
  content={<ProfileForm />}
  onConfirm={handleSave}
  onCancel={handleClose}
/>

// 最佳实践 - 通过子组件组合
<Dialog>
  <DialogTrigger asChild>
    <Button variant="outline">编辑资料</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>编辑资料</DialogTitle>
      <DialogDescription>在此处进行修改</DialogDescription>
    </DialogHeader>
    <ProfileForm />
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="outline">取消</Button>
      </DialogClose>
      <Button onClick={handleSave}>保存</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

Single Responsibility

单一职责

Each component should do one thing well. Signs you need to split:
  • Component file exceeds ~200 lines
  • Multiple unrelated pieces of state
  • Hard to name the component
  • Difficult to test in isolation
每个组件应该只做好一件事。以下迹象表明你需要拆分组件:
  • 组件文件超过约200行
  • 包含多个不相关的状态
  • 难以给组件命名
  • 难以独立测试

Custom Hooks for Reusable Logic

使用自定义Hook复用逻辑

Extract stateful logic into custom hooks:
tsx
// Custom hook encapsulates complexity
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// Component stays simple
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);
  // Use debouncedQuery for API calls
}
将有状态逻辑提取到自定义Hook中:
tsx
// 自定义Hook封装复杂逻辑
function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 组件保持简洁
function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);
  // 使用debouncedQuery进行API调用
}

Keys and Reconciliation

Keys与协调

Key Rules

Key规则

  1. Use stable, unique IDs - preferably from your data
  2. Never use array index for dynamic lists (reordering, filtering, adding)
  3. Never use random values - forces remount on every render
  4. Keys only need sibling uniqueness
tsx
// BAD
{items.map((item, index) => <Item key={index} {...item} />)}
{items.map(item => <Item key={Math.random()} {...item} />)}

// GOOD
{items.map(item => <Item key={item.id} {...item} />)}
  1. 使用稳定、唯一的ID - 优先使用数据中的ID
  2. 动态列表绝不使用数组索引(排序、筛选、添加操作会导致问题)
  3. 绝不使用随机值 - 会导致每次渲染都重新挂载组件
  4. Key只需要在兄弟节点中唯一
tsx
// 不良实践
{items.map((item, index) => <Item key={index} {...item} />)}
{items.map(item => <Item key={Math.random()} {...item} />)}

// 最佳实践
{items.map(item => <Item key={item.id} {...item} />)}

Using Keys to Reset State

使用Key重置状态

Pass a key to reset component state completely:
tsx
// Reset form when editing different user
<UserForm key={userId} user={user} />
通过传递key可以完全重置组件状态:
tsx
// 编辑不同用户时重置表单
<UserForm key={userId} user={user} />

Performance

性能优化

When to Optimize

何时优化

Don't optimize prematurely. Profile first, then optimize bottlenecks.
不要过早优化。先分析性能,再优化瓶颈。

React.memo

React.memo

Wrap expensive pure components:
tsx
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
  return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});
包裹昂贵的纯组件:
tsx
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
  return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});

useMemo for Expensive Calculations

使用useMemo处理昂贵计算

tsx
// Use toSorted() or spread to avoid mutating the original array
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);
tsx
// 使用toSorted()或展开操作避免修改原数组
const sortedItems = useMemo(
  () => [...items].sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

useCallback for Stable References

使用useCallback创建稳定引用

Only needed when passing callbacks to memoized children:
tsx
const handleClick = useCallback((id: string) => {
  setSelected(id);
}, []);

return <MemoizedList items={items} onItemClick={handleClick} />;
仅当需要将回调传递给已缓存的子组件时才需要:
tsx
const handleClick = useCallback((id: string) => {
  setSelected(id);
}, []);

return <MemoizedList items={items} onItemClick={handleClick} />;

Concurrent Rendering for Expensive Updates

使用并发渲染处理昂贵更新

For expensive state updates, prefer concurrent features over aggressive memoization:
tsx
const [isPending, startTransition] = useTransition();

function handleFilter(value: string) {
  setInputValue(value); // Urgent: update input immediately

  startTransition(() => {
    setFilteredItems(expensiveFilter(items, value)); // Non-blocking
  });
}

return (
  <>
    <input value={inputValue} onChange={e => handleFilter(e.target.value)} />
    {isPending && <Spinner />}
    <ItemList items={filteredItems} />
  </>
);
See the Concurrent Rendering section below for full details on
useTransition
and
useDeferredValue
.
对于昂贵的状态更新,优先使用并发特性而非过度缓存:
tsx
const [isPending, startTransition] = useTransition();

function handleFilter(value: string) {
  setInputValue(value); // 紧急:立即更新输入框

  startTransition(() => {
    setFilteredItems(expensiveFilter(items, value)); // 非阻塞
  });
}

return (
  <>
    <input value={inputValue} onChange={e => handleFilter(e.target.value)} />
    {isPending && <Spinner />}
    <ItemList items={filteredItems} />
  </>
);
有关
useTransition
useDeferredValue
的详细信息,请参阅下文的并发渲染部分。

Concurrent Rendering

并发渲染

React 18 introduced concurrent features for keeping the UI responsive during expensive updates.
React 18引入了并发特性,用于在昂贵更新期间保持UI响应。

useTransition

useTransition

Mark state updates as non-blocking so user interactions aren't delayed:
tsx
const [isPending, startTransition] = useTransition();

function handleTabChange(tab: string) {
  startTransition(() => {
    setActiveTab(tab); // Can be interrupted by more urgent updates
  });
}

return (
  <>
    <TabBar activeTab={activeTab} onChange={handleTabChange} />
    {isPending ? <TabSkeleton /> : <TabContent tab={activeTab} />}
  </>
);
Use cases:
  • Search/filter with expensive result rendering
  • Tab switching with heavy content
  • Any state update causing expensive re-renders
将状态更新标记为非阻塞,避免延迟用户交互:
tsx
const [isPending, startTransition] = useTransition();

function handleTabChange(tab: string) {
  startTransition(() => {
    setActiveTab(tab); // 可被更紧急的更新中断
  });
}

return (
  <>
    <TabBar activeTab={activeTab} onChange={handleTabChange} />
    {isPending ? <TabSkeleton /> : <TabContent tab={activeTab} />}
  </>
);
适用场景:
  • 搜索/筛选且结果渲染昂贵
  • 切换包含大量内容的标签页
  • 任何会导致昂贵重渲染的状态更新

useDeferredValue

useDeferredValue

Defer expensive derived values when you don't control the state setter:
tsx
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      <ExpensiveList query={deferredQuery} />
    </div>
  );
}
When to use:
  • Props from parent that change frequently
  • Alternative to debouncing for render performance
  • Showing stale content while fresh content loads
当你无法控制状态设置器时,延迟昂贵的派生值:
tsx
function SearchResults({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  return (
    <div style={{ opacity: isStale ? 0.7 : 1 }}>
      <ExpensiveList query={deferredQuery} />
    </div>
  );
}
适用场景:
  • 来自父组件且频繁变化的props
  • 替代防抖以优化渲染性能
  • 在加载新内容时显示旧内容

useTransition vs useDeferredValue

useTransition vs useDeferredValue

ScenarioUse
You control the state setter
useTransition
Value comes from props
useDeferredValue
Need
isPending
indicator
useTransition
Deferring derived/computed values
useDeferredValue
场景使用
你控制状态设置器
useTransition
值来自props
useDeferredValue
需要
isPending
指示器
useTransition
延迟派生/计算值
useDeferredValue

When NOT to Use

不适用场景

Don't use concurrent features for:
  • Controlled input values (causes typing lag)
  • Quick/cheap state updates
  • State that must stay synchronized
不要在以下场景使用并发特性:
  • 受控输入值(会导致输入延迟)
  • 快速/廉价的状态更新
  • 必须保持同步的状态

Code Splitting

代码分割

Split code into smaller bundles that load on demand.
将代码拆分为更小的包,按需加载。

React.lazy with Suspense

React.lazy与Suspense

tsx
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}
tsx
import { lazy, Suspense } from "react";

const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <Dashboard />
    </Suspense>
  );
}

Route-Based Splitting (Preferred)

基于路由的分割(推荐)

React Router's
lazy
option loads routes in parallel, avoiding waterfalls:
tsx
const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/dashboard", lazy: () => import("./Dashboard") },
  { path: "/settings", lazy: () => import("./Settings") }
]);
This is preferred over
React.lazy
for routes because:
  • Routes load in parallel before rendering
  • React.lazy
    only fetches when the component renders (waterfall)
React Router的
lazy
选项可以并行加载路由,避免请求瀑布:
tsx
const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/dashboard", lazy: () => import("./Dashboard") },
  { path: "/settings", lazy: () => import("./Settings") }
]);
这比针对路由使用
React.lazy
更推荐,因为:
  • 路由在渲染前并行加载
  • React.lazy
    仅在组件渲染时才获取资源(会导致请求瀑布)

Suspense for Loading States

使用Suspense处理加载状态

Use nested Suspense boundaries for progressive loading:
tsx
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<ContentSkeleton />}>
    <MainContent />
  </Suspense>
  <Suspense fallback={<SidebarSkeleton />}>
    <Sidebar />
  </Suspense>
</Suspense>
使用嵌套的Suspense边界实现渐进式加载:
tsx
<Suspense fallback={<PageSkeleton />}>
  <Header />
  <Suspense fallback={<ContentSkeleton />}>
    <MainContent />
  </Suspense>
  <Suspense fallback={<SidebarSkeleton />}>
    <Sidebar />
  </Suspense>
</Suspense>

Error Handling

错误处理

Error Boundaries

错误边界

React requires a class component for error boundaries, or use
react-error-boundary
library:
tsx
// Using react-error-boundary (recommended)
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<ErrorMessage />}>
  <RiskyComponent />
</ErrorBoundary>

// Or with React Router 7, use route-level errorElement
{
  path: "dashboard",
  element: <Dashboard />,
  errorElement: <DashboardError />
}
React要求使用类组件作为错误边界,或使用
react-error-boundary
库:
tsx
// 使用react-error-boundary(推荐)
import { ErrorBoundary } from "react-error-boundary";

<ErrorBoundary fallback={<ErrorMessage />}>
  <RiskyComponent />
</ErrorBoundary>

// 或者在React Router 7中,使用路由级别的errorElement
{
  path: "dashboard",
  element: <Dashboard />,
  errorElement: <DashboardError />
}

Async Error Handling

异步错误处理

Handle errors in loaders/actions, not components:
tsx
// In loader
export async function loader() {
  try {
    const data = await fetchData();
    return { data };
  } catch (error) {
    throw new Response("Failed to load", { status: 500 });
  }
}
在loader/action中处理错误,而非组件中:
tsx
// 在loader中
export async function loader() {
  try {
    const data = await fetchData();
    return { data };
  } catch (error) {
    throw new Response("加载失败", { status: 500 });
  }
}

TypeScript

TypeScript

Props Interfaces

Props接口

Define explicit interfaces, avoid React.FC:
tsx
// GOOD
interface ButtonProps {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
  onClick?: () => void;
}

function Button({ variant = "primary", children, onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{children}</button>;
}
定义明确的接口,避免使用React.FC:
tsx
// 最佳实践
interface ButtonProps {
  variant?: "primary" | "secondary";
  children: React.ReactNode;
  onClick?: () => void;
}

function Button({ variant = "primary", children, onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{children}</button>;
}

Avoid
any

避免使用
any

Use
unknown
when type is truly unknown, then narrow:
tsx
// BAD
function handleError(error: any) {
  console.log(error.message);
}

// GOOD
function handleError(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message);
  }
}
当类型确实未知时使用
unknown
,然后进行类型收窄:
tsx
// 不良实践
function handleError(error: any) {
  console.log(error.message);
}

// 最佳实践
function handleError(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message);
  }
}

Utility Types

工具类型

tsx
// Extend HTML element props
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "secondary";
};

// Children included
type CardProps = React.PropsWithChildren<{
  title: string;
}>;
tsx
// 扩展HTML元素props
type ButtonProps = React.ComponentProps<"button"> & {
  variant?: "primary" | "secondary";
};

// 包含children
type CardProps = React.PropsWithChildren<{
  title: string;
}>;

Accessibility

可访问性

useId for Label Wiring

使用useId关联标签

Use
useId
for accessible form labels - never hardcode IDs:
tsx
function TextField({ label }: { label: string }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  );
}
使用
useId
实现可访问的表单标签 - 永远不要硬编码ID:
tsx
function TextField({ label }: { label: string }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type="text" />
    </div>
  );
}

Focus Management

焦点管理

Manage focus for modals and dynamic content:
tsx
function Modal({ onClose }: { onClose: () => void }) {
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    closeButtonRef.current?.focus();
  }, []);

  return (
    <div role="dialog" aria-modal="true">
      <button ref={closeButtonRef} onClick={onClose}>Close</button>
    </div>
  );
}
为模态框和动态内容管理焦点:
tsx
function Modal({ onClose }: { onClose: () => void }) {
  const closeButtonRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    closeButtonRef.current?.focus();
  }, []);

  return (
    <div role="dialog" aria-modal="true">
      <button ref={closeButtonRef} onClick={onClose}>关闭</button>
    </div>
  );
}

Modal Requirements

模态框要求

Modals must:
  • Trap focus within the modal while open
  • Close on Escape key press
  • Return focus to trigger element on close
  • Prevent background scroll
Prefer proven primitives like Radix UI, Headless UI, or React Aria for complex interactive components (dialogs, dropdowns, tabs). They handle these requirements correctly.
模态框必须:
  • 在打开时将焦点限制在模态框内
  • 按Escape键可关闭
  • 关闭时将焦点返回至触发元素
  • 禁止背景滚动
优先使用成熟的基础组件如Radix UI、Headless UI或React Aria来构建复杂的交互式组件(对话框、下拉菜单、标签页)。它们能正确处理这些要求。

Keyboard Navigation

键盘导航

Ensure all interactive elements are keyboard accessible:
  • Focusable via Tab
  • Activatable via Enter/Space
  • Custom widgets follow WAI-ARIA patterns
确保所有交互元素都支持键盘访问:
  • 可通过Tab键聚焦
  • 可通过Enter/Space键激活
  • 自定义组件遵循WAI-ARIA模式

Common Anti-Patterns to Avoid

需避免的常见反模式

  1. Mutating state directly - Always create new objects/arrays
  2. Over-using Context - Not everything needs global state
  3. Prop drilling vs over-abstraction - 2-3 levels is fine
  4. Storing derived values - Calculate during render
  5. useEffect for everything - Most cases have better alternatives
  6. Premature optimization - Profile first
  1. 直接修改状态 - 始终创建新的对象/数组
  2. 过度使用Context - 并非所有内容都需要全局状态
  3. Prop钻取 vs 过度抽象 - 2-3层钻取是可以接受的
  4. 存储派生值 - 在渲染时计算
  5. useEffect万能论 - 大多数场景有更好的替代方案
  6. 过早优化 - 先分析性能

Reference Documentation

参考文档

For the latest patterns, instruct the agent to query documentation:
  • React docs: Use Context7 with library ID
    /websites/react_dev
  • React Router 7: Use Context7 with library ID
    /remix-run/react-router
  • shadcn/ui: Use Context7 with library ID
    /websites/ui_shadcn
Example query for useEffect alternatives:
Query Context7 /websites/react_dev for "you might not need an effect derived state event handlers"
如需获取最新模式,请指示Agent查询以下文档:
  • React文档:使用Context7,库ID为
    /websites/react_dev
  • React Router 7:使用Context7,库ID为
    /remix-run/react-router
  • shadcn/ui:使用Context7,库ID为
    /websites/ui_shadcn
查询useEffect替代方案的示例:
Query Context7 /websites/react_dev for "you might not need an effect derived state event handlers"

Performance Optimization (Next.js)

性能优化(Next.js)

For in-depth performance optimization patterns, see the Vercel React Best Practices skill:
  • GitHub:
    vercel-labs/agent-skills
    skills/react-best-practices
  • Focus: 57 performance rules covering waterfalls, bundle size, re-renders, hydration
  • Note: Contains Next.js-specific patterns (next/dynamic, server components). Adapt for React Router 7 where applicable, or disregard Next.js-specific guidance when working on non-Next.js projects.
如需深入了解性能优化模式,请参阅Vercel的React最佳实践技能:
  • GitHub
    vercel-labs/agent-skills
    skills/react-best-practices
  • 重点:57条性能规则,涵盖请求瀑布、包大小、重渲染、 hydration
  • 注意:包含Next.js特定模式(next/dynamic、服务端组件)。适用于React Router 7的场景可进行适配,非Next.js项目可忽略Next.js特定指导。