From 23709f6e52bc5562c029014578ba9d7332597568 Mon Sep 17 00:00:00 2001 From: arham766 Date: Thu, 2 Jul 2026 13:29:31 -0700 Subject: [PATCH 1/2] fix(core): make Table column utilities callable from React Server Components The Table barrel (index.ts) carried a 'use client' directive, which marked the pure column utilities re-exported through it (proportional, pixel, generateColumns, paginateData) as client functions: calling them during a server render threw 'Attempted to call proportional() from the server'. Every client module in Table/ already carries its own directive, so the barrel needs none. Adds a source-invariant test pinning both halves of the boundary and documents which parts of the data-driven API are server-safe. Fixes #3457 --- .changeset/table-column-helpers-rsc.md | 6 ++++ packages/core/src/Table/Table.doc.mjs | 2 ++ packages/core/src/Table/index.ts | 6 +++- packages/core/src/Table/rscBoundary.test.ts | 39 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .changeset/table-column-helpers-rsc.md create mode 100644 packages/core/src/Table/rscBoundary.test.ts diff --git a/.changeset/table-column-helpers-rsc.md b/.changeset/table-column-helpers-rsc.md new file mode 100644 index 000000000000..9a7c1f464ddf --- /dev/null +++ b/.changeset/table-column-helpers-rsc.md @@ -0,0 +1,6 @@ +--- +'@astryxdesign/core': patch +--- + +[fix] Table: proportional() and pixel() no longer throw when called from a React Server Component. The Table barrel carried a 'use client' directive that marked the pure column utilities as client functions; the directive now lives only on the component modules, and Table.doc.mjs documents which parts of the data-driven API are server-safe (#3457) +@arham766 diff --git a/packages/core/src/Table/Table.doc.mjs b/packages/core/src/Table/Table.doc.mjs index 99e4568fcf74..377ff8efd0ff 100644 --- a/packages/core/src/Table/Table.doc.mjs +++ b/packages/core/src/Table/Table.doc.mjs @@ -118,6 +118,7 @@ export const docs = { { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' }, { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' }, { guidance: true, description: 'Set explicit width on every column using proportional() or pixel(). proportional(1) gives equal flex distribution with a 120px minimum that prevents columns from collapsing on narrow viewports. Omitting width skips the minimum.' }, + { guidance: true, description: 'Use the data-driven API from React Server Components: proportional(), pixel(), and column definitions without function props are server-safe. Columns using renderCell (or any function prop) need the table wrapped in a "use client" component, since functions cannot cross the server-client boundary.' }, { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' }, { guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' }, { guidance: false, description: 'Omit width on text-heavy columns; without an explicit proportional() width they have no minimum and can squish to near-zero on mobile.' }, @@ -165,6 +166,7 @@ export const docsDense = { { guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' }, { guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' }, { guidance: true, description: 'Set explicit width on every column via proportional() or pixel(). proportional(1) = equal flex w/ 120px min preventing collapse on narrow viewports. Omitting width skips the minimum.' }, + { guidance: true, description: 'Data-driven API is RSC-safe: proportional(), pixel(), column defs w/o function props work in Server Components. renderCell (any function prop) requires a "use client" wrapper.' }, { guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' }, { guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' }, { guidance: false, description: 'Omit width on text-heavy columns; w/o explicit proportional() width they have no minimum and can squish to near-zero on mobile.' }, diff --git a/packages/core/src/Table/index.ts b/packages/core/src/Table/index.ts index d2412b128b48..0701a32cb58c 100644 --- a/packages/core/src/Table/index.ts +++ b/packages/core/src/Table/index.ts @@ -1,6 +1,10 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. -'use client'; +// No 'use client' here: this file only re-exports. Each client module below +// carries its own directive, while the pure column utilities (proportional, +// pixel, generateColumns, paginateData) stay importable from React Server +// Components. A directive on this barrel would mark those helpers as client +// functions and make them throw when called during a server render (#3457). /** * @file index.ts diff --git a/packages/core/src/Table/rscBoundary.test.ts b/packages/core/src/Table/rscBoundary.test.ts new file mode 100644 index 000000000000..57401a683f80 --- /dev/null +++ b/packages/core/src/Table/rscBoundary.test.ts @@ -0,0 +1,39 @@ +// Copyright (c) Meta Platforms, Inc. and affiliates. + +/** + * @file Source invariants for the Table server-client boundary. + * + * The Table barrel (index.ts) must stay free of 'use client' so the pure + * column utilities (proportional, pixel, generateColumns, paginateData) + * remain callable from React Server Components; a directive on the barrel + * marks them as client functions and they throw when called during a + * server render (#3457). The client boundary is carried by each component + * module instead, so this test pins both halves of that arrangement. + */ + +import {describe, it, expect} from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const tableDir = path.dirname(fileURLToPath(import.meta.url)); + +function hasUseClient(file: string): boolean { + const content = fs.readFileSync(path.join(tableDir, file), 'utf-8'); + return /^\s*['"]use client['"];\s*$/m.test(content); +} + +describe('Table RSC boundary', () => { + it('keeps the barrel free of "use client" so column utilities stay server-safe', () => { + expect(hasUseClient('index.ts')).toBe(false); + expect(hasUseClient('columnUtils.ts')).toBe(false); + expect(hasUseClient('types.ts')).toBe(false); + }); + + it('keeps the directive on the client component modules', () => { + expect(hasUseClient('Table.tsx')).toBe(true); + expect(hasUseClient('BaseTable.tsx')).toBe(true); + expect(hasUseClient('TableCell.tsx')).toBe(true); + expect(hasUseClient('TableContext.ts')).toBe(true); + }); +}); From 5055f3ed24e47cc2c1a630f22ca7323d6aab6a81 Mon Sep 17 00:00:00 2001 From: arham766 Date: Thu, 2 Jul 2026 17:59:36 -0700 Subject: [PATCH 2/2] address review: drop the barrel comment and the source-invariant test --- packages/core/src/Table/index.ts | 6 ---- packages/core/src/Table/rscBoundary.test.ts | 39 --------------------- 2 files changed, 45 deletions(-) delete mode 100644 packages/core/src/Table/rscBoundary.test.ts diff --git a/packages/core/src/Table/index.ts b/packages/core/src/Table/index.ts index 0701a32cb58c..1b7d17685d70 100644 --- a/packages/core/src/Table/index.ts +++ b/packages/core/src/Table/index.ts @@ -1,11 +1,5 @@ // Copyright (c) Meta Platforms, Inc. and affiliates. -// No 'use client' here: this file only re-exports. Each client module below -// carries its own directive, while the pure column utilities (proportional, -// pixel, generateColumns, paginateData) stay importable from React Server -// Components. A directive on this barrel would mark those helpers as client -// functions and make them throw when called during a server render (#3457). - /** * @file index.ts * @input Imports from Table component files diff --git a/packages/core/src/Table/rscBoundary.test.ts b/packages/core/src/Table/rscBoundary.test.ts deleted file mode 100644 index 57401a683f80..000000000000 --- a/packages/core/src/Table/rscBoundary.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Meta Platforms, Inc. and affiliates. - -/** - * @file Source invariants for the Table server-client boundary. - * - * The Table barrel (index.ts) must stay free of 'use client' so the pure - * column utilities (proportional, pixel, generateColumns, paginateData) - * remain callable from React Server Components; a directive on the barrel - * marks them as client functions and they throw when called during a - * server render (#3457). The client boundary is carried by each component - * module instead, so this test pins both halves of that arrangement. - */ - -import {describe, it, expect} from 'vitest'; -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import {fileURLToPath} from 'node:url'; - -const tableDir = path.dirname(fileURLToPath(import.meta.url)); - -function hasUseClient(file: string): boolean { - const content = fs.readFileSync(path.join(tableDir, file), 'utf-8'); - return /^\s*['"]use client['"];\s*$/m.test(content); -} - -describe('Table RSC boundary', () => { - it('keeps the barrel free of "use client" so column utilities stay server-safe', () => { - expect(hasUseClient('index.ts')).toBe(false); - expect(hasUseClient('columnUtils.ts')).toBe(false); - expect(hasUseClient('types.ts')).toBe(false); - }); - - it('keeps the directive on the client component modules', () => { - expect(hasUseClient('Table.tsx')).toBe(true); - expect(hasUseClient('BaseTable.tsx')).toBe(true); - expect(hasUseClient('TableCell.tsx')).toBe(true); - expect(hasUseClient('TableContext.ts')).toBe(true); - }); -});