关于 tech-react
为 React 开发者提供组件、Hooks 和 Server/Client 边界方面的实践指南,涉及 React 19 特性。内容强调 Suspense、稳定列表键以及先测量再优化。
主要功能
- 01Hooks 调用规则
- 02Server 与 Client 组件划分
- 03Suspense 异步处理
- 04稳定的列表键
- 05性能测量与优化
适用场景
- 01重构 Hooks 使用方式
- 02将客户端数据获取迁移到 Server Components
- 03排查列表重排后的状态问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ravnhq/ ai-toolkit --skill tech-react安装后在 Claude Code 中输入/tech-react确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
rules/_sections.mdcomponent-no-mutation-callbacks.mdcomponent-semantic-elements.mdcomponent-use-key-for-reset.mdeffect-cleanup.mdeffect-when-not-needed.mdhooks-rules.mdhooks-use-for-promises.mdperf-no-premature-memo.mdperf-stable-keys.mdserver-client-boundaries.md详细介绍
核心挑战
React 的模式会随着每个主要版本不断演进。React 19 引入了用于处理 Promise 的 use() Hook,并通过更明确的客户端/服务端语义,正式确立了 Server Component 的边界。常见问题包括:
- Hook 调用顺序违规:有条件地调用 Hook 会破坏 React 的追踪机制
- 不必要的记忆化:useMemo/useCallback/React.memo 未经测量就会带来额外开销
- 组件过于庞大:混合 Server 和 Client 逻辑会妨碍流式优化
- 缺少 Suspense 边界:没有 Suspense 的异步数据会阻塞整个渲染过程
- 闭包过期:Effect 的依赖数组不正确,或缺少清理逻辑
- 错误使用 key:使用索引作为 key 时,列表重排后状态会关联到错误的列表项
React 19 改进了这些模式:使用 use() Hook 消费 Promise,使用 Server Components 获取数据而无需额外请求,并使用 useTransition() 执行非阻塞更新。
工作流
- 明确组件职责:判断组件应为 Server(数据密集型)还是 Client(交互型)
- 在顶层定义 Hook:所有 Hook 都必须无条件执行,并且位于任何 return 语句之前
- 使用 Suspense 处理异步操作:用 Suspense 边界包裹
use()Hook 调用,并提供加载回退内容 - 使用 key 进行组合:为列表项使用稳定且唯一的 key;使用 key 属性重置组件状态
- 先测量,再优化:在添加记忆化之前,先使用 React DevTools 进行性能分析
- 清理订阅:订阅系统的 Effect 必须始终返回清理函数
- 保持组件精简:提取 Client Components 处理交互,让 Server Components 处理数据
规则
详细模式请参见 规则索引。
示例
正向触发
用户:“重构这个 React 组件,减少重新渲染并让 Hook 的使用更清晰。”
预期行为:使用 tech-react 指南,遵循其工作流,并提供可执行的结果。
正向触发:Server Component 边界
用户:“我正在客户端使用 useEffect 获取数据。该如何使用 Server Components 重构?”
预期行为:将数据获取移至 Server Component,通过 use() Hook 将 Promise 传递给 Client Component,并使用 Suspense 边界包裹。
非触发
用户:“编写一个 Bash 脚本来打包发布构件。”
预期行为:不要优先选择 tech-react;改选更相关的技能,或在不使用该技能的情况下继续处理。
故障排除
Hook 调用违规(“Rendered more hooks than during the previous render”)
- 错误:React 抛出“Rendered fewer/more hooks than during the previous render”
- 原因:在条件、循环或提前 return 语句中调用 Hook
- 解决方案:将 Hook 调用移到任何条件逻辑之前;使用数据 Hook 的
enabled选项跳过执行
Suspense 回退内容始终不显示
- 错误:
use()触发挂起时,加载回退内容没有出现 - 原因:Suspense 边界没有包裹调用
use()的组件 - 解决方案:确保
<Suspense>是该组件的父级,而非兄弟组件
Server Component 无法使用状态或事件
- 错误:useState、onClick 处理函数在 Server Components 中不起作用
- 原因:Server Components 只在服务端渲染一次;它们无法响应客户端交互
- 解决方案:使用 "use client" 指令,将交互部分提取到 Client Components 中;由 Server Component 处理数据获取,并将数据传递给 Client Component
useEffect 运行两次或造成内存泄漏
- 错误:Effect 副作用运行多次;清理函数未执行
- 原因:缺少依赖数组,或缺少返回的清理函数
- 解决方案:添加依赖数组;为订阅/定时器/监听器返回清理函数
列表项失去焦点或顺序错乱
- 错误:输入框焦点在行之间跳转;排序后列表项顺序错乱
- 原因:使用数组索引作为 key,而非稳定且唯一的标识符
- 解决方案:将
key={index}改为key={item.id},使用数据中的唯一属性
Examples: Error Patterns
Error 1: Conditional Hook
Incorrect:
function UserProfile({ userId }: { userId: string | null }) {
if (!userId) {
return <div>Select a user</div>;
}
// Hook called conditionally - React can't track it!
const [profile, setProfile] = useState(null);
return <div>{profile?.name}</div>;
}
Correct:
function UserProfile({ userId }: { userId: string | null }) {
const [profile, setProfile] = useState(null);
// Early return AFTER hooks
if (!userId) {
return <div>Select a user</div>;
}
return <div>{profile?.name}</div>;
}
Error 2: Suspense Without use() Hook
Incorrect:
function Comments({ id }: { id: string }) {
const [comments, setComments] = useState([]);
useEffect(() => {
fetchComments(id).then(setComments);
}, [id]);
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
Correct:
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise);
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
function CommentsSection({ id }: { id: string }) {
return (
<Suspense fallback={<div>Loading comments...</div>}>
<Comments commentsPromise={fetchComments(id)} />
</Suspense>
);
}
Error 3: Server/Client Boundary Confusion
Incorrect:
// BAD: Server Component with useState
async function NotesPage() {
const [selectedNote, setSelectedNote] = useState(null); // ERROR: Can't use state
const notes = await db.notes.getAll();
return (
<div>
{notes.map(note => (
<button onClick={() => setSelectedNote(note)}>
{note.title}
</button>
))}
</div>
);
}
Correct:
// Server Component - fetch data
async function NotesPage() {
const notes = await db.notes.getAll();
return (
<div>
<NotesList notes={notes} />
</div>
);
}
// Client Component - handle interaction
"use client";
function NotesList({ notes }: { notes: Note[] }) {
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
return (
<div>
{notes.map(note => (
<button
key={note.id}
onClick={() => setSelectedNote(note)}
>
{note.title}
</button>
))}
</div>
);
}