react-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Best Practices
React 最佳实践
Preconditions
前置条件
Before applying these practices, confirm:
- Stack check - Verify React Router 7 is in use (or note if using different router/framework)
- TypeScript - Confirm TypeScript is configured with
strict: true - 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.
在应用这些实践之前,请确认:
- 技术栈检查 - 确认正在使用React Router 7(若使用其他路由/框架请注明)
- TypeScript配置 - 确认TypeScript已配置
strict: true - 现有模式对齐 - 评审现有代码库的模式以保持一致性
如果代码库使用其他数据获取方案(TanStack Query、SWR等),请相应调整数据获取指导方案。
Steps
实施步骤
When writing or reviewing React code:
- Audit useEffect usage - For each useEffect, ask "Can this be derived state, an event handler, or handled by the router?"
- Choose state placement - Follow the hierarchy: component → URL → lifted → context
- Verify data fetching - Ensure loaders/actions (or client cache) handle fetching, not raw useEffect
- Check component design - Apply composition patterns, verify single responsibility
- Validate keys - Ensure list keys are stable and unique (not index or random)
- Review TypeScript - Props have explicit interfaces, no types
any - Check accessibility - Semantic HTML, focus management, keyboard support
- Profile if needed - Only add memoization after measuring; consider /
useTransitionfirstuseDeferredValue
在编写或评审React代码时:
- 审计useEffect使用 - 对每个useEffect,思考“这是否可以通过派生状态、事件处理函数或路由来实现?”
- 选择状态存放位置 - 遵循层级顺序:组件内部 → URL → 提升至父组件 → Context
- 验证数据获取方式 - 确保数据获取由loader/action(或客户端缓存)处理,而非直接使用useEffect
- 检查组件设计 - 应用组合模式,验证单一职责原则
- 验证key属性 - 确保列表key稳定且唯一(避免使用索引或随机值)
- 评审TypeScript代码 - Props有明确的接口定义,禁止使用类型
any - 检查可访问性 - 使用语义化HTML、焦点管理、键盘支持
- 按需性能分析 - 仅在测量后添加 memoization;优先考虑/
useTransitionuseDeferredValue
Examples
示例
Example 1: Refactor effect-heavy component
示例1:重构重度依赖effect的组件
User prompt: "This component has too many useEffects, can you clean it up?"
Agent behavior:
- Identify each useEffect and categorize: derived state? event response? data fetch? external sync?
- Convert derived state to render-time calculations
- Move event responses into event handlers
- Replace data fetching with loader or client cache
- Keep only effects that sync with external systems
- Verify cleanup functions exist where needed
用户需求:“这个组件有太多useEffect,能帮忙清理一下吗?”
Agent操作:
- 识别每个useEffect并分类:派生状态?事件响应?数据获取?外部同步?
- 将派生状态转换为渲染时计算
- 将事件响应移至事件处理函数中
- 用loader或客户端缓存替换数据获取逻辑
- 仅保留用于与外部系统同步的effect
- 确认必要的清理函数已存在
Example 2: Convert fetch-in-effect to loader
示例2:将effect中的fetch转换为loader
User prompt: "Migrate this component to use React Router loaders"
Agent behavior:
- Identify the useEffect doing the fetch
- Create a loader function in the route definition
- Replace useState + useEffect with useLoaderData()
- Handle loading states via useNavigation() instead of local state
- Move error handling to the loader or errorElement
- Remove the now-unnecessary effect and state
用户需求:“将这个组件迁移为使用React Router loader”
Agent操作:
- 识别执行fetch的useEffect
- 在路由定义中创建loader函数
- 用useLoaderData()替换useState + useEffect
- 通过useNavigation()处理加载状态,而非本地状态
- 将错误处理移至loader或errorElement
- 删除不再需要的effect和状态
Example 3: Fix list performance issues
示例3:修复列表性能问题
User prompt: "This list re-renders constantly and is slow"
Agent behavior:
- Check key prop - ensure stable IDs, not index or random
- Profile to identify expensive renders
- Wrap list item component in React.memo if pure
- Ensure callbacks passed to items use useCallback
- Check if parent state changes are causing unnecessary re-renders
- Consider virtualization for very long lists
用户需求:“这个列表持续重渲染,运行缓慢”
Agent操作:
- 检查key属性 - 确保使用稳定ID,而非索引或随机值
- 分析性能以识别昂贵的渲染操作
- 如果是纯组件,将列表项组件用React.memo包裹
- 确保传递给子项的回调使用useCallback
- 检查父组件状态变化是否导致不必要的重渲染
- 对于超长列表,考虑使用虚拟化技术
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 without a very good reason. If you think you need to:
exhaustive-deps- The effect probably shouldn't be an effect
- You may need useCallback/useMemo for stable references
- 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- 这个逻辑可能根本不应该用effect实现
- 你可能需要用useCallback/useMemo来创建稳定引用
- 考虑使用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 only when you need to measure DOM or prevent visual flicker:
useLayoutEffecttsx
// 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 unless you see visual flicker that would fix.
useEffectuseLayoutEffect仅当需要测量DOM或避免视觉闪烁时才使用:
useLayoutEffecttsx
// useLayoutEffect - 在DOM变更后同步执行
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setPosition({ top: rect.top, left: rect.left });
}, []);
// useEffect - 在绘制后执行(大多数场景优先使用)
useEffect(() => {
trackPageView();
}, []);优先使用,除非你遇到可以解决的视觉闪烁问题。
useEffectuseLayoutEffectData 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
- - Access loader data
useLoaderData() - - Access action return value (errors, etc.)
useActionData() - - Track navigation/submission state
useNavigation() - - For mutations without navigation
useFetcher()
- - 访问loader返回的数据
useLoaderData() - - 访问action返回值(错误信息等)
useActionData() - - 跟踪导航/提交状态
useNavigation() - - 用于不涉及导航的变更操作
useFetcher()
State Management
状态管理
State Placement Hierarchy
状态存放层级
Place state as close to where it's used as possible:
- Component state - useState for local UI state
- URL state - Query params for shareable state
- Lifted state - Shared parent for sibling communication
- Context - Deeply nested access (use sparingly)
将状态存放在离使用位置尽可能近的地方:
- 组件状态 - 使用useState管理本地UI状态
- URL状态 - 使用查询参数管理可共享状态
- 提升状态 - 共享给父组件以实现兄弟组件通信
- 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:
- for external state stores
useSyncExternalStore - 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规则
- Use stable, unique IDs - preferably from your data
- Never use array index for dynamic lists (reordering, filtering, adding)
- Never use random values - forces remount on every render
- 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} />)}- 使用稳定、唯一的ID - 优先使用数据中的ID
- 动态列表绝不使用数组索引(排序、筛选、添加操作会导致问题)
- 绝不使用随机值 - 会导致每次渲染都重新挂载组件
- 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 and .
useTransitionuseDeferredValue对于昂贵的状态更新,优先使用并发特性而非过度缓存:
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} />
</>
);有关和的详细信息,请参阅下文的并发渲染部分。
useTransitionuseDeferredValueConcurrent 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
| Scenario | Use |
|---|---|
| You control the state setter | |
| Value comes from props | |
Need | |
| Deferring derived/computed values | |
| 场景 | 使用 |
|---|---|
| 你控制状态设置器 | |
| 值来自props | |
需要 | |
| 延迟派生/计算值 | |
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 option loads routes in parallel, avoiding waterfalls:
lazytsx
const router = createBrowserRouter([
{ path: "/", element: <Home /> },
{ path: "/dashboard", lazy: () => import("./Dashboard") },
{ path: "/settings", lazy: () => import("./Settings") }
]);This is preferred over for routes because:
React.lazy- Routes load in parallel before rendering
- only fetches when the component renders (waterfall)
React.lazy
React Router的选项可以并行加载路由,避免请求瀑布:
lazytsx
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 library:
react-error-boundarytsx
// 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-boundarytsx
// 使用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避免使用any
anyUse when type is truly unknown, then narrow:
unknowntsx
// BAD
function handleError(error: any) {
console.log(error.message);
}
// GOOD
function handleError(error: unknown) {
if (error instanceof Error) {
console.log(error.message);
}
}当类型确实未知时使用,然后进行类型收窄:
unknowntsx
// 不良实践
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 for accessible form labels - never hardcode IDs:
useIdtsx
function TextField({ label }: { label: string }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}使用实现可访问的表单标签 - 永远不要硬编码ID:
useIdtsx
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
需避免的常见反模式
- Mutating state directly - Always create new objects/arrays
- Over-using Context - Not everything needs global state
- Prop drilling vs over-abstraction - 2-3 levels is fine
- Storing derived values - Calculate during render
- useEffect for everything - Most cases have better alternatives
- Premature optimization - Profile first
- 直接修改状态 - 始终创建新的对象/数组
- 过度使用Context - 并非所有内容都需要全局状态
- Prop钻取 vs 过度抽象 - 2-3层钻取是可以接受的
- 存储派生值 - 在渲染时计算
- useEffect万能论 - 大多数场景有更好的替代方案
- 过早优化 - 先分析性能
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-skillsskills/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-skillsskills/react-best-practices - 重点:57条性能规则,涵盖请求瀑布、包大小、重渲染、 hydration
- 注意:包含Next.js特定模式(next/dynamic、服务端组件)。适用于React Router 7的场景可进行适配,非Next.js项目可忽略Next.js特定指导。