Skip to content

Commit f2699fa

Browse files
committed
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
1 parent 792c68e commit f2699fa

4 files changed

Lines changed: 52 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@astryxdesign/core': patch
3+
---
4+
5+
[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)
6+
@arham766

packages/core/src/Table/Table.doc.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export const docs = {
118118
{ guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
119119
{ guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
120120
{ 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.' },
121+
{ 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.' },
121122
{ guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
122123
{ guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' },
123124
{ 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 = {
165166
{ guidance: true, description: 'Use density and divider variants to match the information density and scanning needs of your data.' },
166167
{ guidance: true, description: 'Compose rich cell content with Astryx components like Badge, StatusDot, and Avatar via renderCell.' },
167168
{ 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.' },
169+
{ 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.' },
168170
{ guidance: false, description: 'Use a table for data without consistent columns. Use a list or card layout for heterogeneous content.' },
169171
{ guidance: false, description: 'Enable every plugin at once. Add only the features your use case requires to keep the interface focused.' },
170172
{ 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.' },

packages/core/src/Table/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Meta Platforms, Inc. and affiliates.
22

3-
'use client';
3+
// No 'use client' here: this file only re-exports. Each client module below
4+
// carries its own directive, while the pure column utilities (proportional,
5+
// pixel, generateColumns, paginateData) stay importable from React Server
6+
// Components. A directive on this barrel would mark those helpers as client
7+
// functions and make them throw when called during a server render (#3457).
48

59
/**
610
* @file index.ts
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Meta Platforms, Inc. and affiliates.
2+
3+
/**
4+
* @file Source invariants for the Table server-client boundary.
5+
*
6+
* The Table barrel (index.ts) must stay free of 'use client' so the pure
7+
* column utilities (proportional, pixel, generateColumns, paginateData)
8+
* remain callable from React Server Components; a directive on the barrel
9+
* marks them as client functions and they throw when called during a
10+
* server render (#3457). The client boundary is carried by each component
11+
* module instead, so this test pins both halves of that arrangement.
12+
*/
13+
14+
import {describe, it, expect} from 'vitest';
15+
import * as fs from 'node:fs';
16+
import * as path from 'node:path';
17+
import {fileURLToPath} from 'node:url';
18+
19+
const tableDir = path.dirname(fileURLToPath(import.meta.url));
20+
21+
function hasUseClient(file: string): boolean {
22+
const content = fs.readFileSync(path.join(tableDir, file), 'utf-8');
23+
return /^\s*['"]use client['"];\s*$/m.test(content);
24+
}
25+
26+
describe('Table RSC boundary', () => {
27+
it('keeps the barrel free of "use client" so column utilities stay server-safe', () => {
28+
expect(hasUseClient('index.ts')).toBe(false);
29+
expect(hasUseClient('columnUtils.ts')).toBe(false);
30+
expect(hasUseClient('types.ts')).toBe(false);
31+
});
32+
33+
it('keeps the directive on the client component modules', () => {
34+
expect(hasUseClient('Table.tsx')).toBe(true);
35+
expect(hasUseClient('BaseTable.tsx')).toBe(true);
36+
expect(hasUseClient('TableCell.tsx')).toBe(true);
37+
expect(hasUseClient('TableContext.ts')).toBe(true);
38+
});
39+
});

0 commit comments

Comments
 (0)