关于 ideal-react-component
为 React 函数组件提供七段式文件组织规范,并总结常见 Hooks 反模式,面向编写和重构组件的开发者。还说明何时将状态逻辑提取到自定义 Hook。
主要功能
- 01按依赖顺序组织七个代码区段
- 02归纳 Hooks 常见反模式
- 03提供组件重构与 Hook 提取指引
- 04说明早期返回与 Hook 调用顺序
- 05列出按需查阅的深入参考
适用场景
- 01统一 React 函数组件文件结构
- 02排查重复渲染或陈旧状态问题
- 03将复杂组件逻辑提取为自定义 Hook
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ antjanus/ skillbox --skill ideal-react-component安装后在 Claude Code 中输入/ideal-react-component确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
reference/COMPLETE-EXAMPLES.mdEVAL.mdHOOKS-ANTIPATTERNS.mdREFACTORING.mdSECTIONS.md详细介绍
理想的 React 组件结构
概述
函数组件文件采用可预测的七段顺序——导入 → 样式 → 类型 → 组件 → 逻辑 → 条件渲染 → 默认渲染——让任何打开文件的人都知道该看哪里。并配合介绍导致大多数无限循环和状态过时的 hooks 反模式。
**核心原则:**文件按依赖顺序从上到下阅读,最常见的成功路径放在底部,最醒目。它是一种模式,而非硬性规定——小型组件和 React Server Components 合理地省略某些部分。
七段结构
// 1. IMPORTS (grouped: React → third-party → internal @/ → local, blank line between)
import React, { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import styled from 'styled-components';
import { api } from '@/services/api';
import { Button } from './Button';
// 2. STYLED COMPONENTS (prefix "Styled" so they're instantly recognizable)
const StyledContainer = styled.div`padding: 1rem; background: white;`;
// 3. TYPE DEFINITIONS (ComponentNameProps, declared right above the component)
type UserProfileProps = {
userId: string;
onUpdate?: (user: User) => void;
};
// 4. COMPONENT FUNCTION (named export, const arrow function)
export const UserProfile = ({ userId, onUpdate }: UserProfileProps): React.JSX.Element => {
// 5. LOGIC
const [isEditing, setIsEditing] = useState(false); // 5.1 local state
const { data, isLoading, error } = useQuery({ queryKey: ['user', userId], queryFn: () => api.getUser(userId) }); // 5.2 custom/data hooks
useEffect(() => { /* focus the editor */ }, [isEditing]); // 5.3 effects
const displayName = data ? `${data.first} ${data.last}` : ''; // 5.4 post-processing
const handleEdit = () => setIsEditing(true); // 5.5 handlers
// 6. CONDITIONAL RENDERING (exit early for each edge case, after every hook call)
if (isLoading) return <Loading />;
if (error) return <Error message={error.message} />;
if (!data) return <Empty />;
// 7. DEFAULT RENDER (success/happy path stays at the bottom — most visible)
return <StyledContainer>{/* Main JSX */}</StyledContainer>;
};
先写状态,再写其依赖的 hooks 之后的 effect,最后写处理函数——这样每一行只引用上面已声明的内容。
**JavaScript:**采用相同模式,但不带类型注解——跳过第 3 段或使用 JSDoc。
| 段落 | 内容 | 原因 |
|---|---|---|
| 1. 导入 | React、库、内部 @/、本地模块 | 便于查找依赖 |
| 2. 样式 | Styled comps(前缀为 Styled)、Tailwind、CSS Modules | 在视觉上与逻辑分开 |
| 3. 类型 | 组件上方的 *Props、*Return | 一眼就能看到 API |
| 4. 组件 | export const Component = (...) => | 命名导出便于重构 |
| 5. 逻辑 | 状态 → hooks → effects → 后处理 → 处理函数 | 遵守 hook 规则;依赖项先于依赖它们的内容 |
| 6. 条件渲染 | 加载、错误、空状态的提前返回 | 减少嵌套;守卫条件之后类型会收窄 |
| 7. 默认渲染 | 成功状态的 JSX | 成功路径是最醒目的代码 |
常见 hooks 反模式
导致无限循环、数据过时和意外重新渲染的三个最常见原因。先展示推荐写法。
**1. 在渲染期间派生数据;不要用 effect 同步。**只根据其他状态计算状态的 effect 会多触发一次渲染;如果父组件将该值反馈回来,还会直接陷入循环。
const fullName = `${first} ${last}`; // ✅ derive during render
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]); // ❌ extra render, loop risk
**2. 用 key 重置由 prop 派生的状态;不要指望 useState 跟踪 props。**初始化函数只运行一次,因此 prop 改变后,状态仍会过时。
<Component key={itemId} initialValue={data.value} /> // ✅ remount resets state
const [value, setValue] = useState(props.initialValue); // ❌ stale after prop changes
**3. 列出所有依赖项。**遗漏依赖项会捕获过时的闭包——effect 会持续读取首次渲染时的值。
useEffect(() => { setTotal(count * price); }, [count, price]); // ✅ all deps
useEffect(() => { setTotal(count * price); }, [price]); // ❌ missing count
重构
当组件的逻辑超过约 50 行或总行数超过约 200 行时,将有状态逻辑提取到 use[Domain] hook 中——组件专注于呈现,hook 则负责状态和数据流。重构用户指定的组件;有相同问题的兄弟组件或父组件应作为后续事项报告,不要在同一次更改中修改它们。提取标准和组合模式:reference/REFACTORING.md。
注意事项
- 提前返回必须放在所有 Hook 调用之后。 第 6 节排在第 5 节之后有充分的理由,并非为了整洁:在
useEffect上方添加return会导致不同渲染间的 Hook 数量发生变化,React 会抛出“Rendered fewer hooks than expected.”。如果需要通过守卫条件跳过耗费大量资源的工作,应在 Hook 内部进行控制。 - 服务端组件不能包含第 2 节和第 5 节的内容。 在 Next.js App Router 中,文件默认是服务端组件;
useState、useEffect和 styled-components 都要求在文件顶部添加'use client'。构建错误会指出 Hook 名称,而不是缺少的指令。 - 依赖数组中的对象和数组字面量会导致无限循环。
}, [{ id }])或}, [items.filter(...)])会在每次渲染时分配一个全新的标识,因此 effect 会不断重新触发。请将字面量移到 effect 内部,或使用useMemo对其进行记忆化。 - 通过
key重置会重新挂载整个子树。 这会丢弃子组件状态、非受控输入值、焦点和正在运行的动画。当“现在这是另一条记录”时,这样做是合适的;但若只是需要重新初始化单个字段,则不合适。 - 除非事实证明不行,否则优先派生而非记忆化。 只有在计算确实开销很大时才使用
useMemo——模板字符串或对短列表调用.map(),其开销都比记忆化的管理成本低。 - 返回类型是
React.JSX.Element,而不是单独的JSX.Element。 React 19 的类型定义移除了全局JSX命名空间,因此不带限定的写法不再有效。 @tanstack/react-queryv5 接受一个对象参数。 位置参数形式的useQuery(key, fn, options)重载已被移除。v5 还将'loading'状态重命名为'pending',并将isLoading重新定义为isPending && isFetching,因此“尚无数据”现在对应isPending。- GSD 执行阶段不会自动激活技能。 请明确调用
/ideal-react-component,或在项目的 CLAUDE.md 中记录这一约定。
深入参考
遇到相关问题时再加载对应文件——不要一开始就加载。
| 加载 | 适用场景 |
|---|---|
| reference/SECTIONS.md | 各节的 ✅/❌ 详细说明、样式解决方案变体、结构性问题排查 |
| reference/HOOKS-ANTIPATTERNS.md | 包含原理说明的完整反模式集合——上面列出的前三项是摘要 |
| reference/REFACTORING.md | 提取和组合自定义 Hook |
| reference/COMPLETE-EXAMPLES.md | 包含全部七节内容的完整 TS 和 JS 组件 |
| reference/EVAL.md | 激活评估集(仅供维护者使用) |
来源
- 我的理想 React 组件剖析 和 常见 React Hook 反模式 — Antonin Januska
- Hook 规则 · 自定义 Hook · TS React 速查表