- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
TanStack Table 是一套无头(headless)表格库,它负责状态与逻辑(排序、过滤、分页、选择等),而标记与样式完全由你掌控。在 Svelte 适配器中,FlexRender组件是连接"列定义"与"最终 DOM"的桥梁:它把列定义里的header、cell、footer渲染器(字符串、普通值、Svelte 组件或 Snippet)统一解析成真实可见的内容。读完本文,你将掌握FlexRender的完整用法——从最基础的<FlexRender {cell} />快捷形式,到面向组件化表格的renderComponent/renderSnippet,再到聚合行、占位单元格、SSR 与 v8 迁移等高级场景,并结合源码理解其内部的解析决策机制。
从类型别名说起:FlexRender 到底是什么
在 API 参考中,类型别名 FlexRender 的定义极为简洁:
type FlexRender = SvelteComponent;它指向 Svelte 组件基类型,也就是说,在 Svelte 适配器中没有独立的flexRender()函数,只有一个FlexRenderSvelte 组件。这一点与 React/Vue 等适配器不同,也是使用它之前需要首先建立的认知。
组件的真正实现位于 FlexRender.svelte,并通过 src/index.ts 作为具名导出对外发布:
export { default as FlexRender } from './FlexRender.svelte'基础用法:在表格标记中渲染列定义
按照 快速开始指南 中给出的最小示例,普通表格的渲染骨架如下(来自该指南的完整代码):先用createTable创建表格实例,然后在<thead>与<tbody>中遍历表头组和行模型,用FlexRender渲染每个 header 与 cell:
<script lang="ts"> import { createTable, FlexRender, tableFeatures } from '@tanstack/svelte-table' import type { ColumnDef } from '@tanstack/svelte-table' type Person = { firstName: string; lastName: string; age: number } let data = $state<Array<Person>>([ { firstName: 'tanner', lastName: 'linsley', age: 24 }, { firstName: 'tandy', lastName: 'miller', age: 40 }, { firstName: 'joe', lastName: 'dirte', age: 45 }, ]) const features = tableFeatures({}) const columns: Array<ColumnDef<typeof features, Person>> = [ { accessorKey: 'firstName', header: 'First Name', cell: (info) => info.getValue() }, { accessorKey: 'lastName', header: () => 'Last Name' }, { accessorKey: 'age', header: () => 'Age' }, ] const table = createTable({ features, columns, get data() { return data // getter 让表格跟踪 $state rune }, }) </script> <table> <thead> {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} <tr> {#each headerGroup.headers as header (header.id)} <th> {#if !header.isPlaceholder} <FlexRender {header} /> {/if} </th> {/each} </tr> {/each} </thead> <tbody> {#each table.getRowModel().rows as row (row.id)} <tr> {#each row.getAllCells() as cell (cell.id)} <td> <FlexRender {cell} /> </td> {/each} </tr> {/each} </tbody> </table>要点归纳:
header可以是字符串(如'First Name')、返回字符串的函数(如() => 'Age'),或返回组件/片段配置的函数;cell渲染器接收一个 context 对象(包含getValue()、row、column、table等);FlexRender用 Svelte 5 的$derived机制响应式地解析渲染结果,因此当列定义、数据或分组状态变化时,单元格内容会自动更新(rendering.test.ts 中的"Reactive cell"用例验证了这一点)。
三种对象形式:cell、header 与 footer
FlexRender的 Props 是互斥的联合类型,一次只能传入三种对象之一(源码见 FlexRender.svelte 的type Props定义):
| Prop | 渲染内容来源 | 适用场景 |
|---|---|---|
cell | cell.column.columnDef.cell,并以cell.getContext()作为上下文 | 数据行单元格 |
header | header.column.columnDef.header,并以header.getContext()作为上下文 | 表头单元格 |
footer | footer.column.columnDef.footer,并以footer.getContext()作为上下文 | 表尾单元格 |
其中footer 复用 table-core 的Header类型——表尾对象本质上就是一个"表头对象",只是解析时取的是columnDef.footer:
<!-- 表尾:footer 组头对象 --> <FlexRender footer={header} />在 组合表格指南 与迁移文档中可以看到,header与footer的渲染代码几乎一致,唯一的区别是列定义里取header还是footer字段。
分组与聚合单元格的自动决策
当启用行聚合(Row Aggregation)或列分组(Column Grouping)时,同一列在不同行上的显示内容可能不同。FlexRender传入cell对象时会自动做出三层决策(源码 FlexRender.svelte 第 62–99 行的$derived.by块):
- 若
cell.getIsAggregated()为真,优先使用columnDef.aggregatedCell(若未定义则回退到columnDef.cell); - 否则若
cell.getIsPlaceholder()为真,直接渲染空(返回undefined); - 否则使用
columnDef.cell并传入cell.getContext()。
对应的测试用例(rendering.test.ts)验证了三种模式的输出:
expect(outputText('Normal cell')).toBe('cell:Ada') // 普通行 expect(outputText('Aggregated cell')).toBe('aggregate:Ada') // 聚合行 expect(outputText('Placeholder cell')).toBe('') // 分组占位行因此,列定义只需各自声明cell与aggregatedCell,FlexRender会替你做聚合/占位决策,你无需在模板里手写条件判断。
占位表头(isPlaceholder)仍由模板决定
与单元格占位自动渲染为空不同,表头占位符是模板的布局决策。在带有多级表头(header groups)时,浅层表头组中的某些 header 对象是isPlaceholder === true,此时应使用{#if !header.isPlaceholder}包裹,除非你在做跨列表头的布局而有意渲染占位符。指南原文明确说明:
Placeholder headers remain the template's layout decision. Check
header.isPlaceholderunless a spanning-header layout intentionally renders the placeholder.
底层形式:content + context
除了上面推荐的对象快捷形式,FlexRender还保留了旧版更低层级的content+context显式形式:
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />这种形式把"渲染什么"(content,即ColumnDefTemplate)和"用什么上下文调用"(context)分开传入。其 Pros 联合类型中明确标注了content/context与cell/header/footer互斥:
type Props = | { content?: ColumnDefTemplate<...>; context: HeaderContext | CellContext; cell?: never; header?: never; footer?: never } | { cell: Cell; content?: never; context?: never; header?: never; footer?: never } | { header: Header; ... } | { footer: Header; ... }官方指南建议:优先使用对象快捷形式,因为它会替你做聚合单元格与占位符的决策;content/context形式主要用于需要显式控制渲染内容与上下文的高级场景。
renderComponent 与 renderSnippet:组件化单元格
列定义可以返回字符串或普通值,也可以返回 Svelte 组件或 Snippet。为了让FlexRender识别组件与片段,需要先用工具函数包装,这两个工具定义在 render-component.ts:
renderComponent(component, props?)—— 将 Svelte 组件与 props 包装为RenderComponentConfig实例;renderSnippet(snippet, params?)—— 将 Svelte Snippet 与参数包装为RenderSnippetConfig实例(片段必须只接收一个参数)。
完整示例:组件 + 片段混合
指南中的 FlexRender 指南 给出了列定义内同时使用两者的写法:
<script lang="ts"> import { renderComponent, renderSnippet } from '@tanstack/svelte-table' import StatusCell from './StatusCell.svelte' import { nameSnippet } from './snippets.svelte' const columns = columnHelper.columns([ columnHelper.accessor('status', { cell: ({ getValue }) => renderComponent(StatusCell, { status: getValue() }), }), columnHelper.accessor('name', { cell: ({ getValue }) => renderSnippet(nameSnippet, getValue()), }), ]) </script>真实示例:Basic Snippets
仓库中的 basic-snippets 示例 展示了完整的落地用法。它先在snippets.svelte中用 Svelte 5 的{#snippet}定义片段:
{#snippet capitalized(value: string)} <p class="capitalized-text">{value}</p> {/snippet}再在列定义中用renderSnippet包装并传给FlexRender渲染(该示例同时演示了createTableHook+createAppTable的组合用法):
<script lang="ts"> import { createTableHook, FlexRender, renderSnippet } from '@tanstack/svelte-table' import { capitalized, countup, spectrum } from './snippets.svelte' const { createAppTable, createAppColumnHelper } = createTableHook({ features: {} }) const columnHelper = createAppColumnHelper<Person>() const columns = columnHelper.columns([ columnHelper.accessor('firstName', { header: 'First Name', cell: (info) => renderSnippet(capitalized, info.getValue()), }), columnHelper.accessor('progress', { header: 'Profile Progress', cell(info) { return renderSnippet(spectrum, { value: info.getValue(), min: 0, max: 100 }) }, }), ]) const table = createAppTable({ columns, get data() { return data } }) </script> <table> <thead> {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} <tr> {#each headerGroup.headers as header (header.id)} <th> {#if !header.isPlaceholder} <FlexRender header={header} /> {/if} </th> {/each} </tr> {/each} </thead> <tbody> {#each table.getRowModel().rows as row (row.id)} <tr> {#each row.getAllCells() as cell (cell.id)} <td><FlexRender cell={cell} /></td> {/each} </tr> {/each} </tbody> </table>注意:示例中还展示了 Svelte 5 的
createRawSnippet用法(countup片段带 interval 动画),这类"raw snippet"同样可以包装进renderSnippet,说明FlexRender对 Snippet 的兼容面很广。
渲染决策的源码剖析
FlexRender的核心解析逻辑完全基于 Svelte 5 的 runes,没有副作用、没有手动订阅,纯声明式:
<!-- 源码简化,见 packages/svelte-table/src/FlexRender.svelte --> <script lang="ts" generics="..."> // 1. 从互斥 props 中解析出 content 与 context(含聚合/占位决策) const resolved = $derived.by(() => { ... }) // 2. 响应式计算渲染结果 const result = $derived( isFunction(resolved.content) ? resolved.content(resolved.context as any) : undefined, ) </script> {#if typeof resolved.content === 'string'} {resolved.content} {:else if result instanceof RenderComponentConfig} <result.component {...result.props} /> {:else if result instanceof RenderSnippetConfig} {@render result.snippet(result.params)} {:else if result !== undefined} {result} {/if}可见FlexRender的模板按优先级处理四种形态:
- 字符串:直接输出文本(如
header: 'First Name'); RenderComponentConfig:挂载result.component并展开 props;RenderSnippetConfig:通过{@render}调用片段并传入参数;- 其他非
undefined值:直接输出(如函数渲染器返回的普通字符串)。
isFunction判断来自@tanstack/table-core,保证传入的是可调用渲染器时才执行。整条链路在 rendering.test.ts 中被@testing-library/svelte逐一验证,包括普通渲染器、聚合渲染器、占位符、字符串 header、legacycontent/context形式、renderComponent与renderSnippet输出。
App 表(createTableHook)中的 FlexRender
通过createTableHook创建的 App 表(App Table)会额外把FlexRender绑定到表格实例与上下文,详见 createTableHook.svelte。其构建过程(第 637–702 行)把FlexRenderSvelte作为默认组件合并进 cell/header 组件表,并Object.assign到返回的表格对象上:
// cell/header 组件表默认包含 FlexRender const cellComponentsWithFlexRender = { FlexRender: FlexRenderSvelte, ...(cellComponents ?? {}) } const headerComponentsWithFlexRender = { FlexRender: FlexRenderSvelte, ...(headerComponents ?? {}) } // 表格实例直接暴露 FlexRender return Object.assign(table, { AppTable, AppCell, AppHeader, AppFooter, FlexRender: FlexRenderSvelte, ...(tableComponents ?? {}) })因此 App 表有两种使用FlexRender的方式:
方式一:直接导入(普通表与 App 表通用)
正如指南所述,"The direct import shown above works for both ordinary and app tables",即:
<script> import { FlexRender } from '@tanstack/svelte-table' </script> <FlexRender {cell} />方式二:通过表格实例或上下文绑定组件
在 组合表格指南 的组件化渲染中,AppCell/AppHeader/AppFooter包装组件会通过 snippet 把扩展后的实例传给子内容,于是可以在标记中直接使用value.FlexRender(同时还能访问你注册的自定义组件,如header.SortIndicator):
<table.AppTable> <table> <thead> {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} <tr> {#each headerGroup.headers as h (h.id)} <table.AppHeader header={h}> {#snippet children(header)} <th onclick={header.column.getToggleSortingHandler()}> <header.FlexRender {header} /> <header.SortIndicator /> <header.ColumnFilter /> </th> {/snippet} </table.AppHeader> {/each} </tr> {/each} </thead> <tbody> {#each rows as row (row.id)} <tr> {#each row.getAllCells() as cell (cell.id)} <table.AppCell {cell}> {#snippet children(appCell)} <td><appCell.FlexRender cell={appCell} /></td> {/snippet} </table.AppCell> {/each} </tr> {/each} </tbody> </table> </table.AppTable>在类型层面,AppSvelteTable、AppCellContext、AppHeaderContext都声明了FlexRender: typeof FlexRenderSvelte(见 createTableHook.svelte),且useCellContext()/useHeaderContext()返回的实例同样携带 context-bound 的FlexRender,测试 HookHarness.svelte 中的<value.FlexRender cell={value} />、<value.FlexRender header={value} />、<value.FlexRender footer={value} />正是这一特性的直接证据。
服务端渲染(SSR)支持
FlexRender并不依赖 DOM,其解析逻辑与渲染决策是纯同步的,因此可在 SSR 环境直接工作。仓库的 ssr.test.ts 使用svelte/server的render()在无 DOM 环境渲染并断言输出:
// @vitest-environment node test('renders table state and each FlexRender cell mode without a DOM', () => { const { body } = render(SsrHarness) expect(body).toContain('cell:Ada') expect(body).toContain('aggregate:Ada') expect(body).not.toContain('should-not-render') // 占位符不渲染 })这意味着你的表格可以安全地在服务端输出首屏 HTML,FlexRender的聚合/占位决策在 SSR 与客户端行为一致。
从 v8 迁移:flexRender 函数 → FlexRender 组件
在 迁移指南 的"Rendering Changes"一节中,明确给出了 v8 到 v9 的替换方式。v8 中表格渲染通常配合<svelte:component>使用小写的flexRender(...)函数:
<!-- v8 --> <svelte:component this={flexRender(header.column.columnDef.header, header.getContext())} />v9 直接改为FlexRender组件,同时列定义中的 Svelte 组件改用renderComponent、Snippet 改用renderSnippet:
<!-- v9 --> <FlexRender {header} /> <FlexRender {cell} />import { renderComponent } from '@tanstack/svelte-table' import StatusCell from './StatusCell.svelte' const columns = columnHelper.columns([ columnHelper.accessor('status', { cell: ({ row }) => renderComponent(StatusCell, { row }), }), ])<script lang="ts"> import { renderSnippet } from '@tanstack/svelte-table' const columns = columnHelper.columns([ columnHelper.accessor('firstName', { cell: ({ row }) => renderSnippet(nameCell, row), }), ]) </script> {#snippet nameCell(row)} <strong>{row.original.firstName}</strong> {/snippet}如果你的代码库中还有flexRender(...)调用或svelte:component渲染模式,均需按上述方式替换(迁移清单中对应条目为 "ReplaceflexRender(...)and<svelte:component>table rendering with<FlexRender />")。完整迁移对比也可参考适配器的 adapter-migration.md,其中列出了flexRender(...)/<svelte:component>→<FlexRender {cell} />、<FlexRender {header} />、<FlexRender {footer} />的映射表。
最佳实践小结
结合指南 FlexRender 指南 与源码/测试证据,汇总使用建议:
- 优先对象快捷形式:
<FlexRender {cell} />/<FlexRender {header} />/<FlexRender {footer} />,让组件替你完成聚合单元格与占位符决策;仅在需要显式控制时使用content+context形式。 - 组件用
renderComponent,片段用renderSnippet:两者分别返回RenderComponentConfig与RenderSnippetConfig,由FlexRender模板分支识别;renderSnippet的片段必须只接收一个参数。 - 占位表头交给模板:单元格占位符由
FlexRender自动渲染为空,但表头占位符需要在模板中用{#if !header.isPlaceholder}处理。 - App 表双通道:既可直接
import { FlexRender },也可通过table.FlexRender、appCell.FlexRender/header.FlexRender等 context-bound 形式使用,与自定义cellComponents/headerComponents一致。 - SSR 友好:
FlexRender的解析纯声明式、无 DOM 依赖,服务端渲染与客户端行为一致(见 ssr.test.ts)。
想要动手验证这些行为,可运行仓库中的 basic-snippets 示例(组件片段渲染)、查看 组合表格示例(App 表 + 注册组件 + context-bound FlexRender),或直接阅读 FlexRender.svelte 与其测试 rendering.test.ts 深入理解其解析决策。
- 前端
- UI组件
【免费下载链接】table
🤖 Headless UI for building powerful tables & datagrids for TS/JS - React-Table, Vue-Table, Solid-Table, Svelte-Table
相关推荐
TanStack React Table 的 FlexRender 组件:以组件化方式渲染表头、单元格与表尾
TanStack React Table 的 FlexRender 组件:以组件化方式渲染表头、单元格与表尾 本篇技术指南围绕 TanStack Table 仓
前端UI组件@tanstack/svelte-table 灵活渲染指南:FlexRender 组件与 renderComponent / renderSnippet 完整解析
@tanstack/svelte table 灵活渲染指南:FlexRender 组件与 renderComponent / renderSnippet 完整解
前端UI组件TanStack Table(octane-table)FlexRender 组件完全指南:头/单元格/脚注自定义渲染的组件化封装
TanStack Table(octane table)FlexRender 组件完全指南:头/单元格/脚注自定义渲染的组件化封装 导读 FlexRender
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考