React Spectrum API 设计规范:构建统一组件 API 的命名与结构设计准则
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
specs/api/Guidelines.md是 React Spectrum 仓库中为 v3 架构制定的一套组件 API 设计规范,它规定了从布尔属性命名、事件回调、受控/非受控组件,到组件拆分、DOM 属性透传在内的全部 API 设计决策准则。本文完整继承该规范的每一条规则,并结合仓库中 Shared.md(共享 API 基线)、Button 包入口 与 RangeSlider 等真实源码逐一印证规则的落地方式,帮助你在阅读 React Spectrum / react-aria 源码或为自己的组件库设计 API 时,获得一套可对照、可执行的设计方法论。
规范定位:为什么需要统一的 API 设计准则
React Spectrum 采用三层架构(@react-types类型层、@react-stately状态层、@react-aria/@react-spectrum呈现层,参见 2019-v3-architecture.md)。在这样分层的体系中,数百个组件的 API 如果各自为政,使用者的心智成本将急剧上升。因此仓库在 specs/api/ 目录下沉淀了一批 API 规格文档(如 Button.md、TextFields.md),而 specs/api/Guidelines.md 则是所有组件规格文档共同遵守的“元规范”。
规范中还定义了跨组件复用的共享 API 基线,收录在 Shared.md 中,例如输入类组件的InputBase、值类组件的ValueBase、选择类组件的SelectionOptions以及拖放基线DndBase。理解这些基线是理解各条命名规则的前提。
布尔属性(Boolean Props)命名
规范对布尔属性给出了四类前缀约定,分别对应四种语义:
- 表示组件状态的布尔属性,以
is开头。例如isDisabled、isRequired。 - 表示对用户可执行操作的限制的布尔属性,以
allows开头。例如allowsSelection、allowsDuplicates。 - 控制某个选项显示或隐藏的布尔属性,根据默认值选择
show或hide开头:默认可见则用hideXxx(表示可以关掉),默认隐藏则用showXxx(表示可以打开)。 - 控制组件行为的布尔属性,以
should开头。例如shouldFlip、shouldCache。
其余关键规则:
- 大多数布尔属性的默认值应为
false。用户的典型操作是“打开某个选项”,而不是“关闭一个默认开启的选项”。 - 永远不要用
render开头命名布尔属性(如renderIcon),因为它会与 render prop 函数混淆——后者才是真正“渲染该内容”的回调。应改用showIcon。 - 可能合理支持超过两个取值的属性不要用布尔类型,即使当前只支持两种状态。例如用
validationState="invalid"而不是isInvalid,这样未来可以支持validationState="valid"。
仓库源码印证了这套约定:Shared.md 中InputBase接口将isDisabled、isRequired、isReadOnly统一为is前缀的状态属性,而validationState?: 'valid' | 'invalid'正是“为多值扩展预留空间”规则的体现;该属性在 datepicker/Input.tsx、datepicker/DateField.tsx 等输入类组件中实际使用。同时SelectionOptions中的allowsSelection、allowsMultipleSelection、allowsEmptySelection演示了allows前缀的用法。
事件回调(Event Callback Props)命名
规范对事件回调的命名要求:
- 事件回调以
on开头,例如onSelect。 - 如果需要向回调传递值,值作为第一个参数。
- 如果存在事件对象,作为最后一个参数传入。
- 尽可能使用平台无关的事件命名。例如用
onPress而不是onClick,以便支持移动/触摸设备而不仅是鼠标事件。 - 如果事件是某个传入 prop 的变更事件,事件名以
Change结尾,例如onSelectionChange。 onChange只用于对应valueprop;其他变更事件应在on和Change之间加入相关名词,例如onSelectionChange。- 事件名使用现在时态,例如
onChange而不是onChanged。
仓库中可以找到onPress这类平台无关事件的大量使用实例,如 dialog/Dialog.tsx、tag/TagGroup.tsx、table/TableViewBase.tsx 等文件均通过onPress承接按压语义而非点击语义。onChange与value的对应关系则直接体现在共享基线中——Shared.md 的ValueBase<T>接口将value、defaultValue与onChange?: (value: T, e?: Event) => void三者捆绑在一起,正是“值在第一、事件对象在最后”的签名规范。
Children 与 Props 的取舍
规范建议:
- 组件的主内容应使用
children而非字符串 prop。这允许用户放入任意自定义格式(如 JSX),而不是被强制只支持纯文本。 - 内容列表也尽量用
children,例如MenuItemchildren 而不是一个 options 数组。这样用户可以同时自定义每一项的内容和项本身。 - 子组件的命名以主组件名开头,例如
Menu包含MenuItemchildren。 - 对于接受多块内容、
children会产生歧义的组件,主内容取 children,其余作为 props。例如AccordionItem除了children外还有一个titleprop。
Render Props
规范对 render prop 的使用给出四条规则:
- 当部分渲染职责需要委托给用户时,才使用 render prop。
- render prop 以
render开头命名,例如renderItem、renderDragView。 - 将待渲染的项作为参数传给 render prop。
- 一般情况下应优先使用 children;但在 children 无法覆盖的场景使用 render prop,例如虚拟化列表就需要
renderItemprop(因为列表项按需生成,无法预先以 children 形式声明)。
Shared.md 中的DragDelegate.renderDragView(items: any[]) => ReactNode即是一个符合“参数传入待渲染项”规则的 render 型回调。
受控与非受控组件
规范规定:
- 对于用户可以修改的 prop,应同时支持受控值。
- 非受控版本的命名以
default开头,其余部分与受控 prop 同名。例如存在受控的valueprop 时,非受控版本就是defaultValue。
Shared.md 的ValueBase<T>将该模式固化为value?+defaultValue?+onChange?的三件套。
在 slider/RangeSlider.tsx 中可以看到这个约定在实现层的拆解方式:
let {onChange, onChangeEnd, value, defaultValue, getValueLabel, ...otherProps} = props; // ... value: value != null ? [value.start, value.end] : undefined,value与defaultValue从 props 中显式取出并转换后交给底层状态逻辑,其余 props 透传——这正是“受控/非受控双支持”在组件内部的标准落地形态。
方向无关命名(Direction Agnostic Naming)
为了支持 RTL(从右到左)书写方向,规范明确:
- 永远不要用
left或right作为对齐或定位的命名,改用start或end——它们会随书写方向自动映射到左或右,从而让 UI 在 RTL 模式下自动翻转。 - 唯一例外:当确实需要让用户指定绝对的左或右、而不是基于书写方向时,才可以使用
left/right。
这一规则在仓库源码中体现得非常直接:Shared.md 定义了RangeValue<T> { start: T, end: T }与Alignment = 'start' | 'end'类型;RangeSlider 在处理区间值时正是以value.start/value.end访问两端,而非left/right。
基于索引的 Prop 与基于值的 Prop
- 尽可能避免基于索引的 prop,例如
selectedIndex,优先使用基于值的 prop,例如selectedItem。索引会在条目增删时失效,而值保持一致。 - 如果要通过值引用一个子元素,子元素应支持
valueprop。例如MenuItem有valueprop,使Select和ComboBox能按值引用它。
这与 Shared.md 中SingleSelectionBase/MultipleSelectionBase以selectedItem/selectedItems(值数组)而非索引作为受控属性的设计一致。
组件拆分(Splitting Components)
规范给出了两个拆分的判断标准:
- 当选项组合不再合理时,拆分为独立组件。例如
Button和ActionButton的选项不同,就应当是独立组件。 - 当 prop 的类型发生变化时,拆分组件。例如
Slider与RangeSlider接受不同的valueprop——前者是单值,后者是区间。
仓库中的 Button 包 正是这条规则的完整例证,它从一个包中导出了语义各异的按钮族:
export {Button} from '@adobe/react-spectrum/Button'; export {ActionButton} from '@adobe/react-spectrum/ActionButton'; export {FieldButton} from '@adobe/react-spectrum/private/button/FieldButton'; export {LogicButton} from '@adobe/react-spectrum/LogicButton'; export {ClearButton} from '@adobe/react-spectrum/private/button/ClearButton'; export {ToggleButton} from '@adobe/react-spectrum/ToggleButton';Slider(单值)与RangeSlider(区间值,RangeSlider.tsx)并存于同一目录而保持独立组件,同样遵循“值类型不同则拆分”的原则。
属性约束命名(Prop Restrictions)
约束其他 prop 的 prop,应在名称末尾带上主 prop 的名字。例如minValue约束的是value;如果只叫min,会产生歧义——到底约束的是哪个属性?
Shared.md 的RangeInputBase<T>接口将该规则落实为minValue?、maxValue?(另附step?),与ValueBase<T>的value形成明确的约束关系。
DOM 属性透传(DOM Props)
规范对 DOM 属性的透传提出三条要求:
- 所有合法的 DOM prop 都应始终透传到组件根 HTML 元素。
- DOM prop 应与组件的默认计算出的 DOM prop 合并。例如用户的
className应与默认 Spectrum CSS 类名合并,而不是覆盖。 - 事件应与默认实现链式调用。例如用户的
onKeyDown应在组件内部的键盘处理逻辑之外被执行,而不是替代它。
这三条保证了组件在“可定制性”与“内置行为完整性”之间取得平衡:用户覆写样式不破坏组件结构,用户提供事件处理不破坏无障碍与键盘交互逻辑。
子元素定制(Child Element Customization)
当一个组件由多个子元素组合而成时(例如SplitButton组合了两个按钮和一个Menu),规范要求支持childElementPropsprop,以便用户对这些内部子元素做定制(如自定义 CSS 类、测试用的 prop 或其他 DOM prop):
childElementProps是一个从子元素名到 DOM prop 对象的映射。例如SplitButton的菜单触发按钮可通过childElementProps.trigger定制。- 只应支持这些元素的合法 DOM prop,不应支持可能覆盖组件行为的非 DOM prop。
需要说明的是,从当前仓库源码检索看,childElementProps这一具体命名尚未在组件实现中找到同名用法;该条目属于规范层面对“组合型组件如何开放内部子元素定制”给出的设计模式建议,理解它有助于预判组合型组件 API 的演进方向。
通用 Prop 名称(Common Prop Names)
规范要求跨组件复用通用的 prop 名称与共享 API。规范正文列出的通用 prop 取值表如下:
variant = 'a' | 'b' // options dependent on component isQuiet, isEmphasized density = 'compact' | 'regular' | 'spacious' orientation = 'horizontal' | 'vertical' size = 'XS' | 'S' | 'M' | 'L' | 'XL' align = 'start' | 'end' labelPosition = 'top' | 'side' isIndeterminate注意align使用'start' | 'end'而非'left' | 'right',与前述方向无关命名规则首尾呼应;labelPosition = 'top' | 'side'中的side同理不绑定绝对方向。
而规范的最后一句“复用共享 API”指向仓库中的 specs/api/Shared.md,其内容分为三组:
Inputs(输入类):
interface InputBase { isDisabled?: boolean, isRequired?: boolean, validationState?: 'valid' | 'invalid', isReadOnly?: boolean, autoFocus?: boolean } interface ValueBase<T> { value?: T, defaultValue?: T, onChange?: (value: T, e?: Event) => void, } interface TextInputBase { placeholder?: string } interface RangeValue<T> { start: T, end: T } interface RangeInputBase<T> { minValue?: T, maxValue?: T, step?: T } type LabelPosition = 'top' | 'side'; type Alignment = 'start' | 'end'; type NecessityIndicator = 'icon' | 'label'; interface Labelable { label?: ReactNode, isRequired?: boolean, labelPosition?: LabelPosition, labelAlign?: Alignment, necessityIndicator?: NecessityIndicator }Selection(选择类):SelectionOptions(allowsSelection、allowsMultipleSelection、allowsEmptySelection)、MultipleSelectionBase(selectedItems/defaultSelectedItems/onSelectionChange)、SingleSelectionBase(selectedItem/defaultSelectedItem/onSelectionChange),完整演示了allows前缀、default前缀与on...Change命名规则的联动。
Drag and Drop(拖放类):定义了DropOperation(MOVE/COPY/LINK 位掩码)、DropPosition(ON/BETWEEN 位掩码)、DragDelegate、DropDelegate、DataTransferDelegate与ClipboardDelegate等委托接口。其中renderDragView印证了 render prop 命名与传参规则,DropTarget.value则呼应“值优于索引”的原则(value: null表示整个树/表格,配合dropPosition描述落点)。
小结:如何把规范用到实处
这套规范的价值在于把“API 设计”从个人经验变成了可审查的清单。结合仓库的目录组织,可以形成这样的实践路径:
- 设计新组件 API 前,先通读 specs/api/Guidelines.md,逐条对照布尔属性前缀(
is/allows/show/should)、事件命名(on+ 现在时 +Change结尾)、start/end方向命名等规则; - 复用共享基线:从 specs/api/Shared.md 中选取适用的接口(
InputBase、ValueBase、Labelable等),保证value/defaultValue/onChange、minValue/maxValue等命名与全库一致; - 对照既有组件规格(如 Button.md、TextFields.md、Table.md)确认取值范围,再看对应实现文件(如 RangeSlider.tsx)验证受控/非受控与透传逻辑的实际写法。
遵循这套准则,组件 API 才能在可预测性、可扩展性(如validationState的多值演进、start/end的 RTL 翻转)与可组合性之间保持长期一致。
【免费下载链接】react-spectrumA collection of libraries and tools that help you build adaptive, accessible, and robust user experiences.项目地址: https://gitcode.com/GitHub_Trending/re/react-spectrum
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考