组件开发规范(components.md)

1 阅读14分钟

本文档是 React + TypeScript + antd 项目的通用组件编写规范

本文聚焦:组件API设计、组件结构、渲染性能、JSX 写法、状态与副作用、自定义Hook、错误处理。

文章末尾附md文档(可用于项目中skill)

1. 核心规则速览

核心规则速览:编写组件前必读的 12 条 checklist

  1. Props 显式建模(interface XxxProps),禁止 props: unknown / props: any
  2. 函数参数必须显式类型,组件返回类型推荐声明 JSX.Element
  3. 组合优于配置:多区域用 children/slot,不用 showXxx/enableXxx 布尔开关
  4. 显式变体优于 mode/type:<CreateUserForm> / <EditUserForm> 分开
  5. 不在组件内定义组件:子组件提升到模块级
  6. 派生状态不用 effect:render 期可计算的值禁止 useState + useEffect 同步
  7. JSX 内禁止复杂表达式:复杂逻辑抽 useMemo/函数
  8. 条件渲染用三元而非 &&:避免 0 / NaN 被渲染
  9. 状态提升:多子组件共享状态提升到父级或 Provider
  10. 异步三态强制:loading / error / empty / data 必须全处理
  11. 危险操作二次确认:删除/批量操作必须二次确认
  12. 操作反馈:变更请求必须有成功/失败 message 反馈

2. UI 库版本 基线

v5 APIv6 API影响组件
type="primary"color="primary" variant="solid"Button
type="link"color="primary" variant="link"Button
type="text"color="default" variant="text"Button
dangercolor="danger"Button
Button.GroupSpace.CompactButton
bodyStylestyles.bodyCard / Drawer / Modal
headStylestyles.headerCard / Drawer
bordered={false}variant="borderless"Card
Breadcrumb.Itemitems propBreadcrumb
dropdownClassNameclassNames.popup.rootSelect / Cascader / DatePicker
destroyInactivePaneldestroyOnHiddenCollapse / Drawer

v5 团队阅读本文时,Button 行内脚注会标注 v5 等价写法:

<Button color="primary" variant="solid">保存</Button>
// v5: <Button type="primary">保存</Button>

3. 组件 分层 归属

层级职责依赖参考目录命名
纯展示组件纯 UI,不依赖业务 API/Store/权限码仅 propscomponents/ui / components/shared / components/common
业务组件含业务展示逻辑,可主动取数可消费 hooks/storescomponents/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:

  1. 子组件渲染成本高(复杂表格/大量 DOM/重计算)
  2. 父组件频繁更新(输入框/拖拽/滚动)
  3. 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 性能优化原则(停止边界)

性能优化最大的坑是"无依据优化"。三条停止边界:

  1. 先测量后优化:用 React DevTools Profiler / Performance 面板定位瓶颈,不臆测

  2. 三条件全满足才 memo:渲染成本高 + 父频繁更新 + props 稳定(见 6.6)

  3. 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 开头:useModaluseUserListuseDebounce
  • 文件名 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 四态:

状态实现
loadingloading prop / &lt;Skeleton&gt; / &lt;PageSkeleton&gt;
error&lt;EmptyState type="error"&gt; + 重试按钮
empty&lt;EmptyState type="empty"&gt; 或 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文档外展示此内容