Headless 设计哲学

要点

Headless 把行为与外观分开。表格库可以知道排序方向,却不知道你要蓝色按钮还是无边框表头。

什么是 Headless

定义: Headless 库只提供状态、逻辑与无障碍行为,不提供任何 DOM 结构与样式。你拿到的是”引擎”,而不是”整车”。

TanStack Table 给你排序、筛选、分页、行选择的全部状态机,但 <table> 标签长什么样、用什么 CSS 框架,完全由你决定。

核心公式:

Headless 库 = 状态机 + 行为逻辑 + 类型安全
UI 定制     = 你的 JSX + 你的样式方案(Tailwind / CSS Modules / 任意设计系统)

Headless vs 组件库

同样是”带排序的表格”,两种哲学的实现对比:

// 写法 A:组件库(以 Ant Design 为例)
// 一行搞定,但表格外观被锁定在组件库设计体系内
<Table<Person>
  dataSource={data}
  columns={antdColumns}
  onChange={(pagination, filters, sorter) => { /* 受控回调 */ }}
/>
// 写法 B:Headless(TanStack Table v8,React 适配器)
// 表格状态与行模型由库管理;DOM 结构仍完全自主
import { useState } from 'react'
import {
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table'
import type { SortingState } from '@tanstack/react-table'

const [sorting, setSorting] = useState<SortingState>([])
const table = useReactTable({
  data,
  columns,
  state: { sorting },
  onSortingChange: setSorting,
  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
})

// 渲染层完全由你掌控——可以是 <table>,也可以是 <div> 网格
<table className="w-full border-collapse">
  <thead>
    {table.getHeaderGroups().map((hg) => (
      <tr key={hg.id}>
        {hg.headers.map((header) => (
          <th key={header.id} onClick={header.column.getToggleSortingHandler()}>
            {flexRender(header.column.columnDef.header, header.getContext())}
            {{ asc: ' ↑', desc: ' ↓' }[header.column.getIsSorted() as string] ?? ''}
          </th>
        ))}
      </tr>
    ))}
  </thead>
  <tbody>
    {table.getRowModel().rows.map((row) => (
      <tr key={row.id}>
        {row.getVisibleCells().map((cell) => (
          <td key={cell.id}>
            {flexRender(cell.column.columnDef.cell, cell.getContext())}
          </td>
        ))}
      </tr>
    ))}
  </tbody>
</table>

如何选择:

场景更适合原因
中后台标准表单、表格,赶工期组件库开箱即用,风格统一
设计系统自定义强、要求像素级还原Headless渲染层零锁定
需要非标准形态(虚拟滚动表格、看板)Headless组件库改造成本高于自建
团队 UI 水平参差组件库Headless 把质量责任交还给使用者

TanStack 家族一览

库版本职责Headless 体现在
Queryv5服务端状态:缓存/失效/重试/乐观更新与请求库、UI 框架解耦
Tablev8表格状态机:排序/筛选/分组/分页无任何表格 DOM
Routerv1类型安全文件路由内置方案不绑定 UI,导航与布局自定
Formv1表单状态与校验字段渲染完全自主
Startv1基于 Router + Query 的全栈 SSR 框架渲染层可配
Virtual / Ranger / Store 等—虚拟滚动 / 滑块 / 全局状态同样零 UI

为什么 TanStack 选择”无 UI”

  • 关注点分离:数据逻辑与视觉呈现的变化频率不同,分开演化互不拖累
  • 样式方案自由:Tailwind、shadcn/ui、自研设计系统都能无缝接入
  • 逻辑可测试:可将列定义、排序状态和数据转换抽成纯函数;QueryClient 也可在不渲染组件时单测。useReactTable 是 React Hook,应在组件或 Hook 测试环境中调用。
  • 升级独立:UI 重构不必连带重写业务逻辑;TanStack 升级也不污染你的视觉层

代价也要清楚:

  • ❌ 你要自己写全部 JSX 与样式,首屏产出比组件库慢
  • ❌ 无障碍行为(键盘导航、ARIA)需要自己补齐或搭配 Radix UI 等方案

练习与实践

练习一:概念内化

  • [ ] 用自己的话写 3 句话总结 Headless 哲学(不看上文)
  • [ ] 在团队近期项目中找出 2 个”组件库改样式改到痛苦”的场景,判断是否适合 Headless 重写

练习二:对比体验

  • [ ] 跑通 环境搭建 中的排序表格代码,给 <th> 换成 Tailwind 类名
  • [ ] 把同一个 table 实例的渲染改成 <div> 网格布局,验证”渲染层与逻辑层无关”

相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇