本文档是 React + TypeScript + antd 项目的通用组件编写规范
本文聚焦:组件API设计、组件结构、渲染性能、JSX 写法、状态与副作用、自定义Hook、错误处理。
文章末尾附md文档(可用于项目中skill)
1. 核心规则速览
核心规则速览:编写组件前必读的 12 条 checklist
- Props 显式建模(
interface XxxProps),禁止props: unknown/props: any - 函数参数必须显式类型,组件返回类型推荐声明
JSX.Element - 组合优于配置:多区域用 children/slot,不用
showXxx/enableXxx布尔开关 - 显式变体优于 mode/type:
<CreateUserForm>/<EditUserForm>分开 - 不在组件内定义组件:子组件提升到模块级
- 派生状态不用 effect:render 期可计算的值禁止 useState + useEffect 同步
- JSX 内禁止复杂表达式:复杂逻辑抽 useMemo/函数
- 条件渲染用三元而非 &&:避免
0/NaN被渲染 - 状态提升:多子组件共享状态提升到父级或 Provider
- 异步三态强制:loading / error / empty / data 必须全处理
- 危险操作二次确认:删除/批量操作必须二次确认
- 操作反馈:变更请求必须有成功/失败 message 反馈
2. UI 库版本 基线
| v5 API | v6 API | 影响组件 |
|---|---|---|
type="primary" | color="primary" variant="solid" | Button |
type="link" | color="primary" variant="link" | Button |
type="text" | color="default" variant="text" | Button |
danger | color="danger" | Button |
Button.Group | Space.Compact | Button |
bodyStyle | styles.body | Card / Drawer / Modal |
headStyle | styles.header | Card / Drawer |
bordered={false} | variant="borderless" | Card |
Breadcrumb.Item | items prop | Breadcrumb |
dropdownClassName | classNames.popup.root | Select / Cascader / DatePicker |
destroyInactivePanel | destroyOnHidden | Collapse / Drawer |
v5 团队阅读本文时,Button 行内脚注会标注 v5 等价写法:
<Button color="primary" variant="solid">保存</Button>
// v5: <Button type="primary">保存</Button>
3. 组件 分层 归属
| 层级 | 职责 | 依赖 | 参考目录命名 |
|---|---|---|---|
| 纯展示组件 | 纯 UI,不依赖业务 API/Store/权限码 | 仅 props | components/ui / components/shared / components/common |
| 业务组件 | 含业务展示逻辑,可主动取数 | 可消费 hooks/stores | components/feature / components/business |
| 页面组件 | 页面编排 + 页面私有子组件 | 编排层 | pages/{feature}/ |
提升规则:能放页面内就放页面内,仅当被 ≥ 2 页面共用时才提升到公共。
业务组件若需主动取数(消费 hooks/stores),建议文件顶部加注释标注:
// business container
import { useUserList } from '../../hooks/useUserList';
4. 组件 API 设计
4.1 Props 必须显式建模(强制)
- Props 接口必须显式定义
- 函数参数必须显式类型
- 组件返回类型推荐声明
JSX.Element(团队内统一即可) - 可空字段约定:
?: T或?: T | null,团队内统一
interface UserTableProps {
dataSource: UserListItem[];
loading: boolean;
onEdit: (record: UserListItem) => void;
onDelete: (id: string) => void;
}
export function UserTable(props: UserTableProps): JSX.Element {
const { dataSource, loading, onEdit, onDelete } = props;
return (/* ... */);
}
4.2 禁止 any(强制)
- Props / State / 函数参数禁止
any any例外:仅限第三方类型缺失、历史代码兼容、外部输入边界,且必须最小化 + 注释说明原因- 边界类型用
unknown+ 类型守卫,不用any
4.3 组合优于配置
不推荐:配置式(布尔 props 膨胀)
<UserCard
showAvatar
showRole
showDepartment
showActions
actionType="dropdown"
enableEdit
enableDelete
/>
问题:props 膨胀、内部条件分支多、测试组合爆炸。
推荐组合式(children + slot):
<UserCard>
<UserCard.Avatar src={user.avatar} />
<UserCard.Content>
<UserCard.Name>{user.name}</UserCard.Name>
</UserCard.Content>
<UserCard.Actions>
<Button color="primary" variant="solid" onClick={onEdit}>编辑</Button>
</UserCard.Actions>
</UserCard>
4.4 允许 vs 不推荐的 Boolean Props
允许(表达明确状态):loading / disabled / readonly / checked / selected / open / required / danger
不推荐(暗示场景拆分):isEdit / isCreate / showHeader / showFooter / showActions / enableSearch / useSimpleMode
4.5 显式变体优于 mode/type
不推荐
<UserForm mode="create" />
<UserForm mode="edit" />
推荐
function UserFormBase(props: UserFormBaseProps) { /* shared */ }
export function CreateUserForm(props: CreateUserFormProps) {
return <UserFormBase submitText="创建" {...props} />;
}
export function EditUserForm(props: EditUserFormProps) {
return <UserFormBase submitText="保存" {...props} />;
}
4.6 类型职责分离
组件涉及的数据类型应按职责分离,避免复用同一类型:
- 组件 Props:组件入参
- 表单值(FormValues) :表单内部值
- 后端 DTO:后端返回结构
- 提交载荷(Payload) :提交给后端的载荷
四者应独立定义,禁止 type UserFormValues = UserDTO 复用。具体分几层由团队决定,但 Props 与 DTO 必须分离。
5. 组件结构
5.1 单文件组件
无样式 + 无子组件 + 简单类型:
// ConfirmModal.tsx
import { Modal } from 'antd';
import type { ConfirmModalProps } from './types';
export function ConfirmModal(props: ConfirmModalProps): JSX.Element {
const { open, title, onOk, onCancel } = props;
return <Modal open={open} title={title} onOk={onOk} onCancel={onCancel} />;
}
5.2 文件夹组件(有样式或子组件)
EmptyState/
├── EmptyState.tsx # 主组件(与文件夹同名)
├── EmptyState.module.less # 样式(CSS Module / Tailwind / styled-components 任选,团队统一)
├── index.ts # 导出
└── types.ts # Props 类型(复杂类型时独立)
样式方案可选:CSS Module(
.module.less/.module.css)、Tailwind、styled-components、CSS-in-JS 等。团队内统一一种,不混用。
5.3 Compound Components
适用场景:Modal / Drawer / Tabs / TableToolbar / PageHeader / Card / Dropdown / Editor 等天然多子区域组件。
PageCard/
├── index.tsx # Root + Object.assign 导出
├── context.ts # 共享 Context
├── types.ts # Props 类型(含 ContextValue)
├── PageCard.module.less # 样式(方案可选)
└── components/
├── Header.tsx
├── Content.tsx
└── Footer.tsx
Context 接口分三层(state/actions/meta):
interface PageCardContextValue {
state: { bordered: boolean };
actions: { setBordered: (v: boolean) => void };
meta: { hasHeader: boolean };
}
Root 用 Object.assign 挂载子组件:
export const PageCard = Object.assign(PageCardRoot, {
Header: PageCardHeader,
Content: PageCardContent,
Footer: PageCardFooter,
});
5.4 行数限制
- 单文件 > 500 行:强制拆分
- 拆分方向:抽 props / 抽内部子组件 / 抽 hooks
6. 渲染性能
6.1 不在组件内定义组件(HIGH)
每次 render 在组件内部定义新组件类型,React 会 unmount 旧实例 + mount 新实例,丢失所有 state 与 DOM。
// ❌ 禁止:每次 render remount,输入框会失焦
function UserProfile({ user, theme }: Props) {
const Avatar = () => (
<img src={user.avatarUrl} className={theme === 'dark' ? 'dark' : 'light'} />
);
return <Avatar />;
}
// ✅ 推荐:提升到模块级,通过 props 传值
function Avatar({ src, theme }: { src: string; theme: string }) {
return <img src={src} className={theme === 'dark' ? 'dark' : 'light'} />;
}
function UserProfile({ user, theme }: Props) {
return <Avatar src={user.avatarUrl} theme={theme} />;
}
症状:输入框每次击键失焦、动画意外重启。
6.2 派生状态不用 effect(MEDIUM)
源: react-best-practices
rerender-derived-state-no-effect官方: You Might Not Need an Effect
能从 props/state 计算的值,禁止用 useState + useEffect 同步(额外渲染 + 状态漂移)。
// ❌ 禁止:冗余 state + effect
function Form() {
const [firstName, setFirstName] = useState('First');
const [lastName, setLastName] = useState('Last');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
return <p>{fullName}</p>;
}
// ✅ 推荐:render 期派生
function Form() {
const [firstName, setFirstName] = useState('First');
const [lastName, setLastName] = useState('Last');
const fullName = firstName + ' ' + lastName;
return <p>{fullName}</p>;
}
复杂派生用 useMemo:
const filtered = useMemo(() => items.filter(x => x.isActive), [items]);
6.3 函数式 setState(MEDIUM)
更新依赖当前 state 的值时用函数式更新,避免闭包陷阱 + 稳定回调引用。
// ❌ 禁止:闭包陷阱,items 永远是初始值
const removeItem = useCallback((id: string) => {
setItems(items.filter(item => item.id !== id));
}, []);
// ✅ 推荐:函数式更新,引用稳定
const removeItem = useCallback((id: string) => {
setItems(curr => curr.filter(item => item.id !== id));
}, []);
适用:setState 依赖当前值 / useCallback 内引用 state / 异步操作更新 state。
直接更新允许:静态值 setCount(0) / 仅依赖参数 setName(newName)。
6.4 懒初始化 state(MEDIUM)
昂贵初始值用函数形式,避免每次 render 重算。
// ❌ 禁止:buildSearchIndex 每次 render 都跑
const [searchIndex, setSearchIndex] = useState(buildSearchIndex(items));
// ✅ 推荐:仅初始 render 跑一次
const [searchIndex, setSearchIndex] = useState(() => buildSearchIndex(items));
// ✅ 推荐:localStorage 读取也用懒初始化
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings');
return stored ? JSON.parse(stored) : {};
});
适用:localStorage/sessionStorage 读取、构建索引/Map、DOM 读取、重计算。
无需懒初始化:原始值 useState(0) / 引用 props useState(props.value) / 简单字面量 useState({})。
6.5 不用 useMemo 包简单表达式(LOW-MEDIUM)
简单表达式(少数逻辑/算术运算符)且结果是原始类型(boolean/number/string)时,不用 useMemo。
// ❌ 禁止:useMemo 开销大于表达式本身
const isLoading = useMemo(
() => user.isLoading || notifications.isLoading,
[user.isLoading, notifications.isLoading]
);
// ✅ 推荐:直接表达式
const isLoading = user.isLoading || notifications.isLoading;
用 useMemo 的场景:复杂计算 / 大数组 filter/map/sort / 复杂 columns 配置 / 稳定 Provider value / 传给 memo 子组件的 props。
6.6 memo 按需包裹
三条件全满足才用 memo:
- 子组件渲染成本高(复杂表格/大量 DOM/重计算)
- 父组件频繁更新(输入框/拖拽/滚动)
- props 引用稳定(用 useCallback/useMemo 包裹或原始值)
const ExpensiveTable = React.memo(function ExpensiveTable(props: TableProps) {
return <Table dataSource={props.dataSource} columns={props.columns} />;
});
默认包裹 memo 禁止:无依据优化、简单组件、props 不稳定的场景。
6.7 memo 组件的默认非原始 Props(MEDIUM)
memo 组件的可选非原始参数(数组/函数/对象)若不传,会因默认值每次新引用破坏 memo。
// ❌ 禁止:不传 onClick 时,每次 render 新函数引用,破坏 memo
const UserAvatar = memo(function UserAvatar({ onClick = () => {} }: { onClick?: () => void }) {
// ...
});
<UserAvatar />;
// ✅ 推荐:默认值提取为常量
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ onClick = NOOP }: { onClick?: () => void }) {
// ...
});
<UserAvatar />;
6.8 提取昂贵计算到 memo 子组件(MEDIUM)
把昂贵计算提取到 memo 子组件,利用早退避免无谓计算。
// ❌ 禁止:loading 时仍计算 avatar
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user);
return <Avatar id={id} />;
}, [user]);
if (loading) return <Skeleton />;
return <div>{avatar}</div>;
}
// ✅ 推荐:提取到 memo 子组件,loading 时早退
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user]);
return <Avatar id={id} />;
});
function Profile({ user, loading }: Props) {
if (loading) return <Skeleton />;
return <div><UserAvatar user={user} /></div>;
}
6.9 性能优化原则(停止边界)
性能优化最大的坑是"无依据优化"。三条停止边界:
-
先测量后优化:用 React DevTools Profiler / Performance 面板定位瓶颈,不臆测
-
三条件全满足才 memo:渲染成本高 + 父频繁更新 + props 稳定(见 6.6)
-
YAGNI 原则:简单场景不优化,复杂场景先测后优,避免过早抽象
性能问题是工程问题,不是规范问题 — 本规范提供模式,不强制应用。
7. JSX 写法
7.1 条件渲染用三元而非 &&
&& 在条件为 0 / NaN 时会渲染该值。
// ❌ 禁止:count=0 时渲染 "0"
{count && <Badge count={count} />}
// ✅ 推荐:三元明确
{count > 0 ? <Badge count={count} /> : null}
7.2 JSX 内禁止复杂表达式
// ❌ 禁止:JSX 内复杂表达式
<div>{data.filter(x => x.status === 'ENABLED').map(x => `${x.name}(${x.count})`).join(', ')}</div>
// ✅ 推荐:抽 useMemo
const enabledSummary = useMemo(
() => data.filter(x => x.status === 'ENABLED').map(x => `${x.name}(${x.count})`).join(', '),
[data]
);
<div>{enabledSummary}</div>
禁止在 render 内:
- 定义复杂对象/数组/函数(每次 render 新引用,破坏 memo)
- 写复杂业务表达式(抽 useMemo 或函数)
- 定义内部组件(
function Inner() {}在组件体内)
7.3 静态 JSX 提取到模块级
不依赖 props/state 的静态 JSX 提取到模块级,避免每次 render 重建。
// ❌ 禁止:静态 JSX 在组件内
function Header() {
const logo = <div className="logo"><Icon /> Knowledge</div>;
return <header>{logo}</header>;
}
// ✅ 推荐:提升到模块级
const LOGO = (
<div className="logo"><Icon /> Knowledge</div>
);
function Header() {
return <header>{LOGO}</header>;
}
8. 状态与副作用
8.1 状态提升
多个子组件共享状态时,状态必须提升到共同父级或 Provider。
不推荐:状态困在子组件(Toolbar 和 Table 各自维护 selectedRowKeys,不一致)。
推荐:状态提升到父级或 useXxxPageState hook,通过 props 分发给子组件。
8.2 全局状态库订阅规范
无论用 Zustand / Redux / Jotai,都必须按字段订阅,避免整体订阅引发无谓重渲染。
import { useAuthStore } from './stores/authStore';
// ✅ 推荐:按字段订阅
const user = useAuthStore(s => s.user);
const isAuthenticated = useAuthStore(s => s.isAuthenticated);
// ❌ 禁止:整体订阅(任何字段变化都触发重渲染)
const authStore = useAuthStore();
8.3 状态边界
| 状态类型 | 落位 | 示例 |
|---|---|---|
| 跨页面共享 | 状态库(Zustand/Redux/Jotai) | 认证状态、当前项目上下文 |
| 页面局部 | 页面内 useState 或页面 hook | 列表筛选、当前编辑行 |
| 表单字段 | antd Form.useForm() | 表单字段值 |
| 短期依赖注入 | React Context | 主题、当前弹窗上下文 |
| URL 可分享状态 | react-router useSearchParams | 当前 tab、分页 |
禁止:把页面局部状态(如表单临时值)放进全局状态库。
8.4 Context 边界
Context 适合短期依赖注入 / 低频变更场景,不适合高频变更状态。
| 场景 | 用 Context | 用状态库 |
|---|---|---|
| 主题切换 | ✅ | — |
| 当前弹窗上下文 | ✅ | — |
| 当前用户信息(低频变更) | ✅ | ✅ |
| 列表数据(高频变更) | — | ✅ |
| 表单临时值 | — | antd Form |
| 跨页面共享 | — | ✅ |
原则:Context的value变化会让所有消费该 Context 的组件重渲染,高频变更状态用状态库 + 选择器订阅更高效。
8.5 副作用放事件处理器
由用户动作(submit/click/drag)触发的副作用,放事件处理器内,不用 state + effect 模式。
// ❌ 禁止:副作用建模为 state + effect,effect 会因 theme 变化重跑
function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);
useEffect(() => {
if (submitted) {
post('/api/register');
showToast('Registered', theme);
}
}, [submitted, theme]);
return <button onClick={() => setSubmitted(true)}>Submit</button>;
}
// ✅ 推荐:放事件处理器
function Form() {
const theme = useContext(ThemeContext);
function handleSubmit() {
post('/api/register');
showToast('Registered', theme);
}
return <button onClick={handleSubmit}>Submit</button>;
}
8.6 订阅最小化
只在回调内使用的动态状态,不订阅,按需读取。
// ❌ 禁止:订阅整个 searchParams,任何变化都重渲染
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams();
const handleShare = () => {
const ref = searchParams.get('ref');
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
// ✅ 推荐:回调内按需读,不订阅
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search);
const ref = params.get('ref');
shareChat(chatId, { ref });
};
return <button onClick={handleShare}>Share</button>;
}
适用:URL Query / localStorage / sessionStorage 等动态状态仅在回调内用的场景。
9. 自定义 Hook
9.1 命名与单一职责
- Hook 必须以
use开头:useModal、useUserList、useDebounce - 文件名
useXxx.ts,与导出名一致 - 一个 Hook 只做一件事:列表查询、变更操作、弹窗状态、URL 同步
- 不要把列表查询 + 变更操作混在一个 Hook(除非业务极简)
9.2 返回值类型显式
export interface UseUserListResult {
data: UserListItem[];
total: number;
loading: boolean;
error: Error | undefined;
refresh: () => void;
filterProps: { /* 透传给 FilterBar */ };
paginationProps: { /* 透传给 Table */ };
}
export function useUserList(): UseUserListResult {
// ...
}
- 返回值类型必须显式定义
- 禁止
Promise<any>或隐式 any - 复杂返回值用 interface 建模,便于消费方按字段取用
9.3 与组件的边界
- 纯展示组件不直接调 hooks:通过 Props 接收数据
- 业务组件可调 hooks:文件顶部加
// business container注释 - 页面组件(编排层)调 hooks:组合多个 hook,通过 props 分发给子组件
// ❌ 禁止:纯展示组件直接调 hooks
function UserTable() {
const { data } = useUserList(); // 业务逻辑耦合
return <Table dataSource={data} />;
}
// ✅ 推荐:纯展示组件通过 Props 接收
function UserTable(props: UserTableProps) {
return <Table dataSource={props.dataSource} loading={props.loading} />;
}
// ✅ 推荐:业务容器组件调 hooks
// business container
function UserListPage() {
const listHook = useUserList();
return <UserTable dataSource={listHook.data} loading={listHook.loading} />;
}
10. 错误处理与三态
10.1 异步三态强制
每个异步组件必须处理 loading / error / empty / data 四态:
| 状态 | 实现 |
|---|---|
| loading | loading prop / <Skeleton> / <PageSkeleton> |
| error | <EmptyState type="error"> + 重试按钮 |
| empty | <EmptyState type="empty"> 或 Table locale.emptyText |
| data | 正常展示 |
function UserList() {
const { data, loading, error, refresh } = useUserList();
if (loading) return <Skeleton active />;
if (error) return <EmptyState type="error" onRetry={refresh} />;
if (data.length === 0) return <EmptyState type="empty" />;
return <Table dataSource={data} />;
}
10.2 ErrorBoundary 包裹
- 路由级:每个页面 Suspense 外必须包
<ErrorBoundary>,懒加载失败不白屏 - 重型组件级:monaco-editor/xlsx 等重型组件局部包 ErrorBoundary
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<RouteLoading />}>
<LazyPage />
</Suspense>
</ErrorBoundary>
10.3 变更请求反馈
const handleCreate = async (data: CreatePayload) => {
setCreating(true);
try {
await createAPI.create(data);
message.success('创建成功');
refresh();
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败');
} finally {
setCreating(false);
}
};
- 成功必须
message.success,失败必须message.error - loading 独立 useState + try/finally
- 防重复提交(loading 期间禁用按钮)
10.4 危险操作二次确认
TODO: 这里要设计一下全局弹窗和按钮弹窗的使用场景
// 方案一:封装统一确认弹窗(推荐)
ConfirmModal.delete({
title: '确认删除',
content: `删除"${record.name}"后无法恢复`,
onOk: () => handleDelete(record),
});
// 方案二:直接使用 Modal.confirm(不推荐散用)
Modal.confirm({
title: '确认删除',
content: `删除"${record.name}"后无法恢复`,
okText: '确认',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: () => handleDelete(record),
});
建议封装统一确认弹窗,避免散用 Modal.confirm。
10.5 错误态可重试
错误态必须提供重试按钮:
<EmptyState
type="error"
title="加载失败"
description="请稍后重试"
actionText="重试"
onAction={refresh}
/>
11. 可访问性基线
11.1 语义化标签
// ✅ 推荐
<nav>导航</nav>
<main>主内容</main>
<aside>侧边栏</aside>
<section aria-labelledby="title-h"><h2 id="title-h">区块标题</h2></section>
// ❌ 禁止:div 滥用
<div className="nav">导航</div>
11.2按钮必须用 button
// ❌ 禁止:div 假按钮
<div onClick={handleClick} className="btn">点击</div>
// ✅ 推荐
<button onClick={handleClick}>点击</button>
<Button onClick={handleClick}>点击</Button>
11.3 图标按钮必须 aria-label
// ✅ 推荐
<Button
color="primary"
variant="link"
size="small"
icon={<EditOutlined />}
aria-label="编辑"
onClick={handleEdit}
/>
// ❌ 禁止:纯图标无 aria-label
<Button icon={<EditOutlined />} onClick={handleEdit} />
11.4 图片必须 alt
// 内容图:描述清晰
<img src={user.avatar} alt={`${user.name}的头像`} />
// 装饰图:空 alt(屏幕阅读器跳过)
<img src={decorativeBg} alt="" />
// ❌ 禁止:无 alt
<img src={user.avatar} />
11.5 表单 label 关联
// ✅ 推荐:antd Form.Item label 自动关联
<Form.Item label="用户名" name="username" rules={[{ required: true }]}>
<Input placeholder="请输入用户名" />
</Form.Item>
// ❌ 禁止:placeholder 替代 label
<input placeholder="用户名" />
11.6 焦点可见
.focusable {
&:focus-visible {
outline: 2px solid var(--ant-color-primary);
outline-offset: 2px;
}
}
不移除 :focus-visible 样式,保证键盘用户找到焦点。
12 国际化
12.1 两种处理方式
已接入 i18n 的项目
文案走 t('key'),禁止硬编码中文:
// ✅ 推荐
<Button>{t('common.confirm')}</Button>
// ❌ 禁止
<Button>确认</Button>
未接入 i18n 的项目
文案集中管理(页面级 constants.ts),不散落:
// pages/user/constants.ts
export const USER_TEXTS = {
title: '用户管理',
createButton: '新增用户',
emptyTitle: '暂无用户',
deleteConfirm: '删除后无法恢复',
} as const;
import { USER_TEXTS } from './constants';
<EmptyState title={USER_TEXTS.emptyTitle} />
13 禁止项
禁止项总览:以下按章节分组的禁止项清单,涵盖 API 设计、渲染性能、JSX 写法、状态与副作用。
13.1 API 设计
- ❌
props: unknown/props: any - ❌ 布尔 props 控制复杂行为(
showXxx/enableXxx/useXxxMode) - ❌
mode/type控制多场景行为(用显式变体) - ❌ Props 与 DTO 复用同一类型(如
type UserFormValues = UserDTO)
13.2 渲染性能
- ❌ 默认包裹
memo(无三条件依据) - ❌ 派生状态用 useState + useEffect 同步(render 期派生)
- ❌ 闭包陷阱式 setState(
setItems(items.filter(...))不加依赖) - ❌ 昂贵初始值不使用懒初始化(
useState(buildExpensive())) - ❌ 简单表达式用 useMemo 包裹(
useMemo(() => a || b, [a, b])) - ❌ 在组件内定义组件(每次 render remount)
13.3 JSX 写法
- ❌
&&条件渲染可能为0/NaN的值(用三元) - ❌ 静态 JSX 困在组件内(提升到模块级)
- ❌ JSX 内定义复杂对象/数组/函数/内部组件
13.4 状态与副作用
- ❌ 用户动作触发的副作用用 state + effect 模式(放事件处理器)
- ❌ 只在回调内用的动态状态却订阅(按需读取)
- ❌ 整体订阅状态库(
useXxxStore()不加选择器) - ❌ 把页面局部状态放进全局状态库
14. 决策树
14.1 组件创建
暂时无法在唯科之家2.0文档外展示此内容
14.2 状态决策
暂时无法在唯科之家2.0文档外展示此内容
14.3 性能决策
暂时无法在唯科之家2.0文档外展示此内容
14.4 副作用决策
暂时无法在唯科之家2.0文档外展示此内容
15. 例外与豁免
- React Compiler 启用项目:
memo/useMemo可由编译器自动优化,但函数式 setState、派生状态、不在组件内定义组件等规则仍生效(正确性规则,非性能规则) - 老旧代码兼容:暂不强制重构既有违反规则,但新增/修改时按本规范执行
- 第三方组件类型缺失:允许
any但必须最小化 + 注释说明原因 - 特殊场景豁免:如 SSR/SSG、React Native 等场景,本规范部分规则(如懒加载、CSS Module)不适用,团队另行约定
SKILL.md
暂时无法在唯科之家2.0文档外展示此内容