From 6d5a66bfe98838b0a30625ee76e8b5f2db6da2f6 Mon Sep 17 00:00:00 2001 From: Yash Lunawat Date: Sun, 13 Sep 2026 00:31:06 +0530 Subject: [PATCH] fix(vue-table): render nothing for an empty string cell flexRender returns the value it is given for primitives, so a cell or header whose value is an empty string produces an empty text vnode. Vue's server renderer emits nothing for it, the client creates a text node, and hydration reports a children mismatch inside the cell. Return null for an empty string, matching how flexRender already treats null and undefined, so server and client agree. --- .changeset/flexrender-empty-string.md | 5 ++ packages/vue-table/src/FlexRender.ts | 8 +++ .../tests/unit/flexRenderHydration.test.ts | 49 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 .changeset/flexrender-empty-string.md create mode 100644 packages/vue-table/tests/unit/flexRenderHydration.test.ts diff --git a/.changeset/flexrender-empty-string.md b/.changeset/flexrender-empty-string.md new file mode 100644 index 0000000000..08f4d509d2 --- /dev/null +++ b/.changeset/flexrender-empty-string.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-table': patch +--- + +Render nothing instead of an empty string in `flexRender`, so a cell whose value is `''` no longer causes a hydration mismatch under SSR. diff --git a/packages/vue-table/src/FlexRender.ts b/packages/vue-table/src/FlexRender.ts index b257a2dd4a..6a10376a1d 100644 --- a/packages/vue-table/src/FlexRender.ts +++ b/packages/vue-table/src/FlexRender.ts @@ -32,6 +32,10 @@ export function flexRender(render: any, props: any): any { return render } + if (render === '') { + return null + } + if (typeof render === 'function') { const rendered = render(props) @@ -39,6 +43,10 @@ export function flexRender(render: any, props: any): any { return rendered } + if (rendered === '') { + return null + } + if (isVNode(rendered)) { return rendered } diff --git a/packages/vue-table/tests/unit/flexRenderHydration.test.ts b/packages/vue-table/tests/unit/flexRenderHydration.test.ts new file mode 100644 index 0000000000..ba3f4939af --- /dev/null +++ b/packages/vue-table/tests/unit/flexRenderHydration.test.ts @@ -0,0 +1,49 @@ +// @vitest-environment jsdom + +import { afterEach, describe, expect, test, vi } from 'vitest' +import { createSSRApp, defineComponent, h } from 'vue' +import { renderToString } from 'vue/server-renderer' +import { FlexRender, flexRender } from '../../src/FlexRender' + +afterEach(() => { + vi.restoreAllMocks() +}) + +function makeApp(cellValue: string) { + return defineComponent({ + setup() { + return () => + h('table', [ + h('tbody', [ + h('tr', [h('td', [h(FlexRender, { render: cellValue })])]), + ]), + ]) + }, + }) +} + +describe('FlexRender hydration', () => { + test('renders nothing for an empty string', () => { + expect(flexRender('', {})).toBeNull() + expect(flexRender(() => '', {})).toBeNull() + }) + + test('hydrates an empty string cell without a mismatch', async () => { + const App = makeApp('') + const html = await renderToString(createSSRApp(App)) + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) + const error = vi.spyOn(console, 'error').mockImplementation(() => {}) + const container = document.createElement('div') + + container.innerHTML = html + createSSRApp(App).mount(container, true) + + const messages = [...warn.mock.calls, ...error.mock.calls].map((args) => + String(args[0]), + ) + + expect(messages.filter((message) => message.includes('Hydration'))).toEqual( + [], + ) + }) +})