如何在 React 中重拾原生 HTML 属性

 更新时间:2025年11月20日 08:44:19   作者:zane  
本文将从基础到深入,拆解为什么在 React 组件中大量使用原生属性(尤其是 data-*)是一种更专业、更可维护、更高性能的工程实践,感兴趣的朋友跟随小编一起看看吧

在现代 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 状态。例如:

<button data-state="open" aria-expanded="true">Menu</button>

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

Radix DropdownMenu 的 Trigger

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

Radix 始终同步 data-state 与 aria-expanded——
这样即便 React 状态层出故障,ARIA 与 DevTools 都能明确显示组件状态。

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

传统方式:

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

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

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

完全不需要额外 JS 逻辑。

Tailwind 示例:

<div data-state="open" class="transition data-[state=open]:opacity-100 data-[state=closed]:opacity-0">
</div>

Radix 的 Tabs Root

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

<Tab data-state={selected ? 'active' : 'inactive'} />

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-*:

<div data-disabled data-orientation="vertical"></div>

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

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

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

  • 重新执行组件函数
  • diff 虚拟 DOM
  • 再决定是否更新 DOM

但若使用 data-*:

React 只需更新一次根节点的属性。
子组件无需 re-render。

Radix Accordion

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

<AccordionTrigger data-state={open ? 'open' : 'closed'} />

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

这种模式特别适合:

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

6. 调试友好

React 状态调试有几个问题:

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

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

<button data-state="open" data-disabled="true">...</button>

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

Radix 团队在 RFC 中提到:

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

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

下面根据源码梳理一张类图,逻辑示意,展示 Radix 组件的状态是如何“外溢”到 DOM 属性的:

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

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

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

8. Radix DropdownMenu

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

Trigger

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)

<Content
  data-state={open ? 'open' : 'closed'}
  data-side={side}
  data-align={align}
>
  {children}
</Content>

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

[data-state="open"][data-side="bottom"] {
  animation: slideDown 200ms;
}

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

9. 总结

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

维度优势
可访问性与 ARIA 标准兼容,屏幕阅读器更容易识别状态
样式化CSS 可直接响应状态,不需要 JS 驱动 class 切换
性能减少不必要 re-render,复杂组件收益巨大
框架无关性状态直接存在 DOM,可跨框架复用
调试DevTools 可见属性,定位问题更直接
工程化支持 Tailwind、设计系统、主题系统等工具

到此这篇关于如何在 React 中重拾原生 HTML 属性的文章就介绍到这了,更多相关React HTML 属性内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • react中JSX的注意点详解

    react中JSX的注意点详解

    这篇文章主要为大家详细介绍了react中JSX的注意点,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • 在react中使用highlight.js将页面上的代码高亮的方法

    在react中使用highlight.js将页面上的代码高亮的方法

    本文通过 highlight.js 库实现对文章正文 HTML 中的代码元素自动添加语法高亮,具有一定的参考价值,感兴趣的可以了解一下
    2022-01-01
  • 使用React+ts实现无缝滚动的走马灯详细过程

    使用React+ts实现无缝滚动的走马灯详细过程

    这篇文章主要给大家介绍了关于使用React+ts实现无缝滚动的走马灯详细过程,文中给出了详细的代码示例以及图文教程,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2023-08-08
  • React中使用Redux Toolkit状态管理的示例详解

    React中使用Redux Toolkit状态管理的示例详解

    在现代 React 应用程序中,状态管理是一个至关重要的部分,使用 Redux Toolkit 可以简化 Redux 的配置和管理,本文将通过三个文件的示例,详细讲解如何使用 Redux Toolkit 创建和管理一个简单的计数器状态,需要的朋友可以参考下
    2024-11-11
  • React实时预览react-live源码解析

    React实时预览react-live源码解析

    这篇文章主要为大家介绍了React实时预览react-live源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • React component.forceUpdate()强制重新渲染方式

    React component.forceUpdate()强制重新渲染方式

    这篇文章主要介绍了React component.forceUpdate()强制重新渲染方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • React Native自定义标题栏组件的实现方法

    React Native自定义标题栏组件的实现方法

    今天讲一下如何实现自定义标题栏组件,我们都知道RN有一个优点就是可以组件化,在需要使用该组件的地方直接引用并传递一些参数就可以了,这种方式确实提高了开发效率。对React Native自定义标题栏组件的实现方法感兴趣的朋友参考下
    2017-01-01
  • React中常用的一些钩子函数总结

    React中常用的一些钩子函数总结

    这篇文章给大家总结了React中常用的一些钩子函数,文中通过代码示例给大家介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-01-01
  • React钩子函数之useDeferredValue的基本使用示例详解

    React钩子函数之useDeferredValue的基本使用示例详解

    useDeferredValue是React 18中非常有用的一个钩子函数,它可以帮助我们优化渲染性能,并让UI更加流畅,如果你还没有尝试过它,不妨在你的下一个React项目中试一试,这篇文章主要介绍了React钩子函数之useDeferredValue的基本使用,需要的朋友可以参考下
    2023-08-08
  • 在react中使用 indexDb的方法

    在react中使用 indexDb的方法

    在React中使用IndexedDB进行前端离线存储,可以存储大量数据,支持复杂的数据类型和高性能查询,通过示例展示了如何创建数据库、添加数据、查询数据和构建一个简单的待办事项应用,感兴趣的朋友跟随小编一起看看吧
    2024-11-11

最新评论