关于 react-best-practices
react-best-practices 是面向 React 开发的技能,帮助构建易维护、可测试的应用。内容涵盖 React 18/19 模式、组件设计、测试与基于测量的性能优化。
主要功能
- 01选择服务端或客户端组件
- 02Hooks 与组件组合模式
- 03错误边界与 Suspense
- 04Context 与渲染优化
- 05面向行为的无障碍测试
适用场景
- 01划分组件的数据与状态职责
- 02设计可复用的 React 组件
- 03测试加载、错误与交互状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ pixel-process-ug/ superkit-agents --skill react-best-practices安装后在 Claude Code 中输入/react-best-practices确认已加载。
详细介绍
React 最佳实践
概述
应用现代 React 模式,构建易于维护、性能出色且可测试的应用程序。本技能涵盖 React 18/19 的功能,包括服务端组件、Hooks 最佳实践、组件组合、错误边界、Suspense、Context 优化和渲染性能。本技能补充 senior-frontend 技能,深入介绍 React 专属内容。
开始时请说明:「我将使用 react-best-practices 技能来介绍 React 专属模式。」
阶段 1:分析组件需求
目标: 编码前了解组件的职责和数据需求。
操作
- 确定组件的单一职责
- 确定数据需求(服务端数据还是客户端数据)
- 选择服务端组件(默认)或客户端组件
- 确定状态管理需求
- 规划错误和加载状态
服务端与客户端决策表
| 需求 | 组件类型 | 原因 |
|---|---|---|
| 直接获取数据(DB、API) | 服务端(默认) | 无需客户端 JS,速度更快 |
| 事件处理器(onClick、onChange) | 客户端('use client') | 需要浏览器交互能力 |
| useState / useReducer | 客户端 | 状态需要客户端运行时 |
| useEffect / useLayoutEffect | 客户端 | 副作用需要客户端 |
| 浏览器 API(window、localStorage) | 客户端 | 服务端没有浏览器 |
| 使用客户端功能的第三方库 | 客户端 | 该库需要客户端 |
| 无需交互能力 | 服务端(默认) | 包体积更小,速度更快 |
停止 — 完成以下事项前不要进入阶段 2:
- 已定义组件职责(单一用途)
- 已做出服务端与客户端的选择,并说明理由
- 已梳理数据需求
阶段 2:使用恰当的模式实现
目标: 根据组件需求应用正确的 React 模式。
操作
- 应用恰当的组合模式
- 正确实现 Hooks
- 添加错误边界和 Suspense
- 根据性能分析结果优化渲染
- 编写验证行为的测试
停止 — 完成以下事项前不要进入阶段 3:
- 模式符合组件的实际需求
- 没有不必要的复杂性(不进行过早优化)
- 测试覆盖用户可见的行为
阶段 3:测试与验证
目标: 通过测试验证组件行为。
操作
- 使用可访问性查询编写测试
- 测试用户交互及其结果
- 测试错误和加载状态
- 验证可访问性
查询优先级(React Testing Library)
| 优先级 | 查询 | 用途 |
|---|---|---|
| 第 1 优先级 | getByRole | 任何带 ARIA 角色的元素 |
| 第 2 优先级 | getByLabelText | 表单字段 |
| 第 3 优先级 | getByPlaceholderText | 没有标签的字段 |
| 第 4 优先级 | getByText | 非交互元素 |
| 最后 | getByTestId | 其他方法均不适用时 |
停止 — 满足以下条件即表示测试完成:
- 用户交互产生预期结果
- 已测试错误状态
- 可访问性检查通过
Hooks 最佳实践
useState
// Functional updates for state based on previous state
setCount(prev => prev + 1);
// Lazy initialization for expensive initial values
const [data, setData] = useState(() => computeExpensiveInitialValue());
// Group related state
const [form, setForm] = useState({ name: '', email: '', role: 'user' });
useEffect
依赖数组规则
- 包含组件作用域内所有会随时间变化的值
- 在 effect 内部定义函数,或使用 useCallback 包装函数
- 绝不要谎报依赖项(ESLint:
react-hooks/exhaustive-deps)
清理模式
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch(url, { signal: controller.signal });
const data = await res.json();
setData(data);
} catch (e) {
if (e.name !== 'AbortError') setError(e);
}
}
fetchData();
return () => controller.abort();
}, [url]);
何时不应使用 useEffect
| 不使用 useEffect 的场景…… | 改用 |
|---|---|
| 获取数据 | React Query、SWR 或服务端组件 |
| 转换数据 | 在渲染期间计算 |
| 用户事件 | 事件处理器 |
| 同步外部存储 | useSyncExternalStore |
自定义 Hooks 规则
- 名称以
use开头 - 封装可复用的有状态逻辑
- 每个关注点使用一个 Hook
- 超过 2 个值时返回对象(而非数组)
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 Tabs({ children, defaultValue }: TabsProps) {
const [activeTab, setActiveTab] = useState(defaultValue);
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext.Provider>
);
}
Tabs.Tab = function Tab({ value, children }: TabProps) {
const { activeTab, setActiveTab } = useTabsContext();
return (
<button role="tab" aria-selected={activeTab === value} onClick={() => setActiveTab(value)}>
{children}
</button>
);
};
Tabs.Panel = function Panel({ value, children }: PanelProps) {
const { activeTab } = useTabsContext();
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
};
组合方式决策表
| 模式 | 适用场景 | 示例 |
|---|---|---|
| 复合组件 | 相关组件共享隐式状态 | 标签页、手风琴、菜单 |
| 插槽(子组件) | 复杂内容布局 | 包含页眉/主体/页脚的卡片 |
| Render Props | 子组件需要父组件数据以灵活渲染 | 使用自定义渲染的 DataFetcher |
| 高阶组件 | 横切关注点(旧版) | withAuth、withTheme |
| 自定义 Hook | 无需 UI 的可复用有状态逻辑 | useDebounce、useLocalStorage |
插槽模式
// Prefer composition over props for complex content
// Bad
<Card title="Hello" subtitle="World" icon={<Star />} actions={<Button>Edit</Button>} />
// Good
<Card>
<Card.Header>
<Card.Icon><Star /></Card.Icon>
<Card.Title>Hello</Card.Title>
</Card.Header>
<Card.Actions>
<Button>Edit</Button>
</Card.Actions>
</Card>
错误边界
放置策略决策表
| 级别 | 用途 | 示例 |
|---|---|---|
| 路由级别 | 捕获页面级崩溃 | Next.js 中的 error.tsx |
| 功能级别 | 隔离功能故障 | 为每个主要部分添加包装 |
| 数据级别 | 包装异步数据组件 | 放在 Suspense 边界周围 |
| 不要放在叶子级别 | 粒度过细,会增加冗余 | 不要为单个按钮添加包装 |
Suspense
// Nested Suspense for granular loading
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<MainContent />
</Suspense>
</Suspense>
Context 优化
问题:Context 会导致不必要的重新渲染
解决方案决策表
| 技术 | 适用场景 | 示例 |
|---|---|---|
| 按更新频率拆分 Context | 部分值更新频繁,部分值很少更新 | ThemeContext(较少更新)与 UIStateContext(频繁更新) |
| 对 Context 值进行记忆化 | Provider 使用相同数据重新渲染 | useMemo(() => ({ state, dispatch }), [state]) |
| 使用选择器(Zustand/Jotai) | 需要细粒度订阅 | useStore(state => state.user.name) |
| 向上提升状态 | 只有父组件需要重新渲染 | 将数据作为 props 传给记忆化子组件 |
渲染优化
记忆化决策表
| 技术 | 适用场景 | 不适用场景 |
|---|---|---|
React.memo | 使用相同 props 频繁渲染,且重新渲染开销大 | 每次渲染时 props 都会变化 |
useMemo | 开销大的计算,或依赖项需要引用相等性 | 简单计算 |
useCallback | 为记忆化子组件提供稳定的函数引用 | 函数不会作为 prop 传递 |
| 无(默认) | 始终从这里开始 | 过早优化 |
规则: 记忆化之前先进行性能分析。过早记忆化是最常见的 React 反模式。
虚拟化
对于超过 100 项的列表:
import { useVirtualizer } from '@tanstack/react-virtual';
Server Component 规则
- 不能使用 hooks
- 不能使用浏览器 API
- 不能将函数作为 props 传给 Client Components
- 可以导入并渲染 Client Components
- 可以向 Client Components 传递可序列化数据
// Server Component — fetches data directly
async function UserProfile({ userId }: { userId: string }) {
const user = await db.user.findUnique({ where: { id: userId } });
return (
<div>
<h1>{user.name}</h1>
<UserActions userId={userId} /> {/* Client Component child */}
</div>
);
}
// Client Component — handles interactivity
'use client';
function UserActions({ userId }: { userId: string }) {
const [isFollowing, setIsFollowing] = useState(false);
return <Button onClick={() => toggleFollow(userId)}>Follow</Button>;
}
反模式/常见错误
| 反模式 | 错误原因 | 正确做法 |
|---|---|---|
使用 useEffect 获取数据 | 竞态条件、没有缓存、无法去重 | 使用 React Query 或 Server Components |
| props 逐层传递超过 2 层 | 耦合紧密、维护困难 | 使用组合、Context 或 Zustand |
| 存储派生状态 | 可以计算得出的状态是不必要的状态 | 在渲染期间计算 |
使用 useEffect 从 props 同步状态 | 不必要的 effect、闭包过期 | 在渲染期间派生状态,或使用 key prop |
| 单体组件(超过 200 行) | 难以阅读、测试和维护 | 提取子组件 |
| 动态列表使用索引作为 key | 协调错误、状态过期 | 使用稳定的唯一 ID |
| 直接操作 DOM | 绕过 React 协调机制 | 谨慎使用 refs,优先使用状态 |
| 直接测试状态值 | 属于实现细节,重构时容易失效 | 测试用户可见的结果 |
| 对所有内容进行记忆化 | 增加复杂性,且通常更慢 | 先分析性能,再进行优化 |
文档查询(Context7)
使用 mcp__context7__resolve-library-id,然后使用 mcp__context7__query-docs 获取最新文档。返回的文档优先于记忆中的知识。
react— 用于 hooks、context、suspense、server components 或 React 19+ 的变更next.js— 用于 App Router 模式、数据获取或 server actions
集成点
| 技能 | 关系 |
|---|---|
senior-frontend | 前端技能使用本技能中的 React 模式 |
testing-strategy | React 测试遵循策略金字塔 |
clean-code | 组件代码遵循整洁代码原则 |
performance-optimization | React 渲染优化遵循测量方法 |
webapp-testing | 端到端测试验证 React 组件行为 |
code-review | 代码审查检查 React 反模式 |
acceptance-testing | UI 验收标准驱动组件测试 |
技能类型
灵活 — 根据具体的 React 版本、项目结构和团队惯例应用这些模式。原则是一致的,但实现细节可能有所不同。优化前务必先进行性能分析。
评论
支持 Markdown