关于 react
面向项目开发者的 React 组件编写指南,涵盖样式迁移、组件选择、状态管理和渲染优化,并结合项目中的 @lobehub/ui 与 StyleX 约定。
主要功能
- 01StyleX 样式编写与迁移
- 02按优先级选择 UI 组件
- 03状态所有权与组件拆分
- 04Popover 关闭处理
- 05渲染性能与记忆化规则
- 06布局组件使用规范
适用场景
- 01为现有组件迁移 StyleX
- 02为界面选择合适的基础组件
- 03梳理组件状态与复用边界
- 04检查 Popover 导航后的关闭行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ lobehub/ lobehub --skill react安装后在 Claude Code 中输入/react确认已加载。
附带文件references/layout-kit.md
详细介绍
React 组件编写指南
样式
| 场景 | 方法 |
|---|---|
| 已使用 StyleX 的文件和新的 SPA 样式 | stylex.create + @lobehub/ui/stylex/cssVar.stylex / @lobehub/ui/stylex/media.stylex。SPA 编译器是 plugins/vite/stylex.ts(@stylexjs/unplugin,前缀为 lh)。不要将这些令牌模块复制到应用中。 |
| 尚未迁移的现有样式 | createStaticStyles + cssVar.*(零运行时,模块级) |
| 简单的一次性样式 | 内联 style 属性 |
真正动态的样式(如 readableColor/chroma 等 JS 颜色函数) | createStyles + token — 最后手段 |
StyleX
- 新代码不得从
@lobehub/ui导入createStaticStyles/createStyles/css/keyframes/createGlobalStyle/injectGlobal(stylex-migration/no-emotion-styles)。仍在使用这些 API 的文件列于scripts/stylexMigration/legacyFiles.json;迁移某个文件后,运行bun run stylex:migration --prune-legacy。不要添加条目。 - 一次迁移整个文件。StyleX 长写规则具有更高的特异性(
.x:not(#\#)),会覆盖同一元素上的 emotion 类;但 StyleX 简写规则不会。因此,元素只迁移了一部分时,会按属性进行解析。 - 同时转发调用方
className/style的元素:使用来自@/styles/sx的{...sx([styles.root, active && styles.active], className, style)}。在className旁展开stylex.props会触发@stylexjs/no-conflicting-props。 - 允许调用方重新设置样式的组件:接受
xstyle?: StyleXStyles,并将其放在sx/stylex.props数组的最后。外部传入的普通className无法覆盖 StyleX 规则。向@lobehub/ui组件传入类名(className、classNames)则有效,因为 lobe-ui CSS 位于@layer lobe-ui中。 - 值遵循
@stylexjs/valid-styles:不支持多值简写(margin: '0 8px'→marginBlock/marginInline),也不支持字符串形式的flex(flex: '1')。 background、animation、border和border{Top,Right,Bottom,Left}简写无论值是什么都会编译为空类(包括border: 'none'),且不会报错。请改用长写属性:backgroundColor(替换background简写时还要加上backgroundImage: 'none')、animationName/animationDuration/ …、borderWidth/borderStyle/borderColor。- 在开发环境中使用 DevDock 快照辅助工具检查迁移:在已迁移的页面运行
__lobeStyleSnapshot.capture('<name>', '<root selector>'),切回旧文件(git stash push -- <path>),重新加载,运行__lobeStyleSnapshot.compare('<name>'),然后运行git stash pop。每组结果按[captured, current]排列,此处为[migrated, original]。工具会列出跟踪的计算样式、布局框(相对于根元素)或文本存在差异的元素以及::before/::after;空列表表示这些内容一致,并不代表已检查悬停、焦点或关闭状态的对话框,因此请打开迁移涉及的每种状态并进行比较。保持窗口大小不变,将根元素内的所有滚动区域滚回起点,并将指针移出比较区域。 bun run stylex:migration --list <A|B|C|D>会按迁移难度列出文件:A 为平铺样式,B 为自身伪类,C 为后代 / 祖先耦合(使用stylex.defaultMarker()+stylex.when.ancestor()或xstyle),D 为标签 / 第三方 DOM 选择器(使用作用域受限的纯 CSS)。
组件优先级
src/components— 项目专用的可复用组件@lobehub/ui/base-ui— 无头基础组件。如果组件位于这里,请使用它。不要导入同名的根导出。@lobehub/ui— 更高级的组件 / 封装 antd 的组件(仅在没有对应的 base-ui 组件时使用)- antd — 仅在 base-ui 和
@lobehub/ui根导出都未提供该组件时使用 - 自定义实现 — 真正的最后手段
如果不确定有哪些可用组件,请搜索现有代码,或查看 node_modules/@lobehub/ui/es/index.mjs 和 node_modules/@lobehub/ui/es/base-ui/。
@lobehub/ui/base-ui — 这些组件始终优先使用
| 组件 | 导入 |
|---|---|
Alert(+ AlertProps) | import { Alert, type AlertProps } from '@lobehub/ui/base-ui'; |
Select(+ SelectProps、SelectOption) | import { Select } from '@lobehub/ui/base-ui'; |
Modal(命令式 API) | import { createModal, confirmModal, useModalContext, type ModalInstance } from '@lobehub/ui/base-ui'; |
DropdownMenu | import { DropdownMenu } from '@lobehub/ui/base-ui'; |
ContextMenu | import { ContextMenu } from '@lobehub/ui/base-ui'; |
Popover | import { Popover } from '@lobehub/ui/base-ui'; |
ScrollArea | import { ScrollArea } from '@lobehub/ui/base-ui'; |
Switch | import { Switch } from '@lobehub/ui/base-ui'; |
Toast | import { Toast } from '@lobehub/ui/base-ui'; |
FloatingSheet | import { FloatingSheet } from '@lobehub/ui/base-ui'; |
Drawer | import { Drawer } from '@lobehub/ui/base-ui'; |
关于 Modal,请参阅专门的 modal 技能——优先使用命令式的 createModal({ content: … }) 模式,而不是旧版声明式的 <Modal open … /> 模式。SPAGlobalProvider 中已经挂载了 base-ui 自己的 ModalHost。
常见疏漏:
import { Select } from '@lobehub/ui'看起来没问题,但它是基于 antd 的Select。请使用 base-ui 的Select。Modal、DropdownMenu等组件也一样。
@lobehub/ui 根模块 — base-ui 没有对应组件时使用
| 类别 | 组件 |
|---|---|
| 通用 | ActionIcon、ActionIconGroup、Block、Button、Icon |
| 数据展示 | Avatar、Collapse、Empty、Highlighter、Markdown、Tag、Tooltip |
| 数据录入 | CodeEditor、CopyButton、EditableText、Form、Input、InputPassword、SearchBar、TextArea |
| 布局 | Center、DraggablePanel、Flexbox、Grid、Header、MaskShadow |
| 导航 | Burger、Menu、SideNav、Tabs |
状态
将临时状态保留在最小且合适的持有者中。当状态转换和处理程序影响渲染,或形成可复用单元时,提取自定义 Hook;不要仅仅因为组件使用了特定数量的 Hook 就进行提取。
只有在建立真实的状态、复用、渲染更新或可挂载能力边界时,才拆分组件。不要仅仅为了缩小文件而拆分;当文件达到 source-layout 中的 1200 行限制时,应寻找上述边界,而不是按行数切分。将繁重的领域功能分解为由宿主组装的原子组件,属于 compose-atoms 的职责。
弹出层的退出
弹出层内容中的操作如果会离开弹出层,就必须关闭它,否则弹出层会停留在目标页面上方(例如,用户面板中的套餐徽标导航到 /settings/plans)。
- 导航:内容包含链接或会进行导航的宿主需要保留
open状态,调用@/hooks/useCloseOnNavigate中的useCloseOnNavigate(open, () => setOpen(false)),并将返回的 key 传给其Popover。这样也能处理之后添加的链接,包括来自其他文件的链接。当导航导致<Activity>中的宿主被隐藏时,key 尤其重要(例如首页侧边栏):隐藏的树不会设置新 effect,因此 base-ui 弹出层无法完成退出动画,会一直显示,直到重新挂载将其移除。 - 模态框交接或最终选择:如
NavPanel/switcher/SwitcherMenu.tsx所示,在打开模态框之前或完成选择之后调用usePopoverContext().close()(或宿主的 setter)。在弹出层外部渲染的内容不能使用该上下文,因为在开发环境中,默认的close会抛出异常。 - 确认步骤的结果会显示在同一弹出层中时(例如分享链接的隐私确认),保持弹出层打开。
渲染性能与记忆化
将 memo、useMemo 和 useCallback 视为可选的优化手段,而不是默认的组件包装方式。添加之前,先确定实际的重新渲染边界,并优先考虑结构性调整:
- 在更新边界处分拆。
- 将临时状态移至最小范围的所有者。
- 使用范围精确的 Zustand 选择器,避免宽泛订阅。
不要对没有 props 或渲染内容很简单的组件进行记忆化,也不要对通常会收到新对象、数组、函数或 JSX 子元素的组件进行记忆化。不要用记忆化来弥补状态位于组件树过高位置的问题。
只有当子树确实开销较大或频繁重复渲染、其相关输入在父组件的正常渲染期间保持稳定,并且性能分析或具体的渲染路径分析表明记忆化可以避免相应工作时,才使用记忆化。在实现摘要或 PR 中说明理由。
布局
使用 @lobehub/ui 中的 Flexbox 和 Center。完整的 props 和示例请参阅 references/layout-kit.md。
- 使用
gap而非margin来设置弹性子元素之间的间距 - 使用
flex={1}填充可用空间 - 对复杂布局使用嵌套的 Flexbox;为可滚动区域设置
overflow: 'auto'
相关技能
ux:加载视觉效果(场景 →references/feedback.md§4.1 中的加载器表格)和面向用户的交互设计。modal:命令式 base-ui 模态框模式。spa-routes:SPA 导航、路由归属、路由器配置和.desktop变体。compose-atoms:将繁重的领域功能拆分为可挂载的能力原子;每个宿主只导入其挂载的部分。zustand:状态存储结构和选择器约定。