在 React 中重拾原生 HTML 属性

在现代 React 组件开发中,优先想到 useState、useEffect、context、props drilling 这样的框架能力,而容易忽略:
浏览器原生 HTML 属性本身,就是一个强大而成熟的状态表达载体。

比如 data-* 为代表的自定义属性,在近几年被越来越多的专业组件库采用,如 Radix UI、Headless UI、Ark UI 等。

本文将从基础到深入,拆解为什么在 React 组件中大量使用原生属性(尤其是 data-*)是一种更专业、更可维护、更高性能的工程实践。

1. data-*:语义扩展与原生兼容性

HTML 原生属性有一个重要优势:
它们天生是"被设计来给用户代理(浏览器、辅助工具)理解的"。

而 data-* 作为 HTML5 制定的可扩展机制:

  • 保证语法合法
  • 不破坏 HTML 自身语义
  • 与 ARIA 标准兼容
  • 支持 CSS、JS 原生读取

这意味着使用 data-* 做状态表达,是天然符合浏览器和工具链的方式。

2. 提升可访问性

在构建无障碍(a11y)兼容组件时,一种错误做法是:

把组件状态(如 open/closed)全部存储在 React 内部,屏幕阅读器却读不到。

但如果将状态同步到 data-state、data-disabled,辅助工具就能更轻松感知 UI 状态。例如:

html 复制代码
<button data-state="open" aria-expanded="true">Menu</button>

屏幕阅读器可以根据 ARIA 属性直接宣布状态,而 data-state 也能作为冗余状态标识用于调试和样式。

tsx 复制代码
<DropdownMenuPrimitive.Trigger
  data-state={open ? "open" : "closed"}
  aria-expanded={open}
>
  {children}
</DropdownMenuPrimitive.Trigger>

Radix 始终同步 data-statearia-expanded ------

这样即便 React 状态层出故障,ARIA 与 DevTools 都能明确显示组件状态。

3. 简化样式化:CSS 直接响应状态,避免 JS 再渲染

传统方式:

  • React 改状态 → 组件重新渲染 → className 改变 → 样式变化

而 data-* 提供了更直接、无阻塞的方式:

css 复制代码
[data-state="open"] {
  opacity: 1;
  transform: scale(1);
}

[data-state="closed"] {
  opacity: 0;
  transform: scale(0.95);
}

完全不需要额外 JS 逻辑。

Tailwind 示例:

html 复制代码
<div data-state="open" class="transition data-[state=open]:opacity-100 data-[state=closed]:opacity-0">
</div>

Radix 的 Tabs Root

Radix 的 Tabs Root 会给触发项注入:

tsx 复制代码
<Tab data-state={selected ? 'active' : 'inactive'} />

CSS 直接响应:

css 复制代码
[data-state="active"] {
  color: var(--accent);
}

优点总结

  • 更少的 JS 参与 意味着更快
  • 避免 React re-render 意味着更稳定
  • 样式只靠 CSS cascade 意味着更干净

4. 框架无关性

React 的 className、state、useMemo、useCallback 仅存在于虚拟 DOM 中。

而 data-* 写在真正的 DOM 节点上:

  • 测试工具(Playwright、Cypress)可直接选择
  • 浏览器可直接识别
  • SSR 与 SEO 可直接读取
  • 迁移框架时不受代码结构影响(例如迁移到 Vue/Solid/Svelte)

Radix UI 做得最极致的一点:

它所有组件都输出没有样式的 "primitive DOM 节点",

而状态全部映射为 data-*:

html 复制代码
<div data-disabled data-orientation="vertical"></div>

使之成为一套真正的 headless 组件协议,而不是 React 专属 DSL。

5. 性能优化:减少不必要的 React re-render

如果用 className 或 props 作为状态传递,当状态变化时,React 必须:

  1. 重新执行组件函数
  2. diff 虚拟 DOM
  3. 再决定是否更新 DOM

但若使用 data-*:

React 只需更新一次根节点的属性。

子组件无需 re-render。

Radix Accordion

Accordion 内容展开时只更新触发器的 data-state:

tsx 复制代码
<AccordionTrigger data-state={open ? 'open' : 'closed'} />

内容本身不会重新渲染,不会额外执行 useEffect、useLayoutEffect。

这种模式特别适合:

  • 大型表格组件
  • 虚拟滚动
  • 菜单、Popover、Tooltip 等频繁开合的复杂交互

6. 调试友好

React 状态调试有几个问题:

  • useState 值在 DevTools 中需要额外打开 React 面板
  • className 合成后难以识别状态来源
  • 在复杂组件中状态链路不清晰

但 data-* 让调试变得"肉眼直观":

html 复制代码
<button data-state="open" data-disabled="true">...</button>

你不用打开任何插件,就能立刻看到每个节点的状态。

Radix 团队在 RFC 中提到:

data-state 与 data-disabled 的主要目的之一,就是增强可调试性。

7. 案例剖析:Radix 的 data-* 状态模型

下面根据源码梳理一张类图,逻辑示意,展示 Radix 组件的状态是如何"外溢"到 DOM 属性的:
graph LR A[React State Hook<br/>useControllableState] -->|derives state| C[Radix Component Logic<br/>e.g. useMenuContext] C -->|expose state| D[DOM Node<br/>data-state attributes]

Radix 的数据流是一种精心设计的"漏斗":

  1. React 层管理逻辑
  2. 计算状态
  3. 把状态下沉到 DOM 原生属性
  4. CSS / ARIA / 工具链再根据这些属性响应

这是一种非常解耦的模型。

@radix-ui/react-dropdown-menu 为例。

Trigger

tsx 复制代码
const Trigger = React.forwardRef((props, ref) => {
  const open = useDropdownMenuContext();

  return (
    <Primitive.button
      ref={ref}
      data-state={open ? 'open' : 'closed'}
      aria-expanded={open}
      {...props}
    />
  );
});

触发器只负责把状态表达为:

  • data-state
  • aria-expanded

完全不关心样式、动画、布局。

菜单内容(Content)

tsx 复制代码
<Content
  data-state={open ? 'open' : 'closed'}
  data-side={side}
  data-align={align}
>
  {children}
</Content>

这些 data-* 使得 CSS 可以精确选择:

css 复制代码
[data-state="open"][data-side="bottom"] {
  animation: slideDown 200ms;
}

从而达到 交互逻辑与展示逻辑彻底分离

9. 总结

工作为求效率使用框架合情合理,但个人学习不能只看框架,有时候学学 HTML 也不错,哈哈,甚至可以帮助我们更好地使用框架。最后总结下各项优势:

维度 优势
可访问性 与 ARIA 标准兼容,屏幕阅读器更容易识别状态
样式化 CSS 可直接响应状态,不需要 JS 驱动 class 切换
性能 减少不必要 re-render,复杂组件收益巨大
框架无关性 状态直接存在 DOM,可跨框架复用
调试 DevTools 可见属性,定位问题更直接
工程化 支持 Tailwind、设计系统、主题系统等工具
相关推荐
挫折常伴左右1 小时前
初见HTML
前端·html
一水鉴天19 小时前
整体设计 定稿 之24 dashboard.html 增加三层次动态记录体系仪表盘 之2 程序 (Q208 之1)
前端·html
一水鉴天20 小时前
整体设计 定稿 之22 dashboard.html 增加三层次动态记录体系仪表盘 之1
前端·html
沟通QQ8762239651 天前
有限元仿真模型仿真模型-基于COMSOL多物理场耦合仿真的变压器流固耦合及振动噪声分析 1、变...
html
江公望1 天前
HTML5 History 模式 5分钟讲清楚
前端·html·html5
A24207349301 天前
使用jQuery动态操作HTML和CSS
css·html·jquery
carry杰2 天前
nacos bootstrap.yml 动态配置开发测试线上模式
前端·bootstrap·html
€绘梨衣2 天前
笔墨屋12.12题目及解析
html
Tiam-20162 天前
安装NVM管理多版本node
vue.js·前端框架·node.js·html·es6·angular.js
ewenge2 天前
springboot+Selenium 实现html转图片(内含驱动包)
spring boot·selenium·html