Skip to content

Commit 888d799

Browse files
bloveclaude
andauthored
fix(react)!: make stable row identity consistent across every entry point (#404)
* fix(react): make optional-when-conventional row identity one contract `PretableSurfaceRowsProps` declared `getRowId` non-optional while the docs, `usePretable`, `useLocalRowModel` and `createLocalRowModel` all treated it as optional for rows with a conventional `id`. The engine has always agreed with the docs — `createLocalRowModel` falls back to `row.id`, `usePretable` drops an `undefined` accessor before forwarding, and `<PretableSurface>` already passed `getRowId` through unconditionally — so the component prop types were the side that was wrong, and a reader following the docs hit a type error on their first typed grid. Omitting the prop was not even a clean failure. Because `getRowId` was a required member of an intersection-of-union props type, its absence made inference for the whole call bail to the type-parameter defaults, so the reported error was a `TColumns` mismatch against `readonly PretableColumn<object>[]` rather than a missing prop. Declaring it optional restores inference: `TRow`, `TRowId` and `TColumns` now come from `rows` and `columns` alone. `getRowId` is now optional on `<PretableSurface>`, `<Pretable>` and `<LabeledGridSurface>`, with the new `PretableRowIdRequirement` intersected in to re-require it for any row shape without an `id: string | number`. That guard was missing on `usePretable` too, which silently accepted id-less rows and resolved `TRowId` to `never`, deferring the failure to a `PretableRowModelError` on the first row read; `UsePretableRowsOptions` is now gated the same way `CreateLocalRowModelWithDefaultIdOptions` always was. `<Pretable>` forwards its props as one object rather than prop by prop, because re-listing `getRowId` erases the unresolved conditional the surface requires. Covered by a type test under `typecheck:public` and by runtime tests that a selection and a focused cell stay pinned to the same row across a row-set update — fresh objects, reordered, with `id` deliberately disagreeing with array position — plus a case proving an explicit `getRowId` still wins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: regenerate react.api.md after the final format pass --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 62d2bcd commit 888d799

11 files changed

Lines changed: 700 additions & 130 deletions

File tree

‎apps/website/content/docs/grid/api-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Both helpers return native `Intl.NumberFormatOptions` and are exported from `@pr
6565

6666
`rows` and `model` are mutually exclusive. Rows mode diffs each new `rows` prop into its long-lived local model. Explicit-model mode makes the supplied model the only data/derivation owner; compatible `columns` may override presentation only.
6767

68-
Rows with a conventional string or number `id` do not require `getRowId`. Otherwise pass a stable explicit accessor. Identity survives row-array replacements and is used by selection, focus, editing, expansion, and transactions.
68+
Rows with a conventional string or number `id` do not require `getRowId`. Otherwise it is required, and omitting it is a compile error rather than a runtime failure. This is one contract across every entry point that takes rows — `<Pretable>`, `<PretableSurface>`, `usePretable`, `useLocalRowModel`, and `createLocalRowModel` all treat the accessor the same way. Identity survives row-array replacements and is used by selection, focus, editing, expansion, and transactions.
6969

7070
Important shared props include `ariaLabel`, `viewportHeight`, `overscan`, `locale`, `state`, `onSelectionChange`, `onRowSelectionChange`, `onFocusChange`, `onPaste`, `onCopy`, `copyToClipboard`, `csvOptions`, `onExport`, `saveFile`, `groupPanel`, `groupColumn`, `hideGroupedColumns`, and cell/header/row render hooks. Rows mode alone accepts the exact controlled `query`/`onQueryChange` pair, `onRowChange`, `initialExpansion`, and `aggregateFilteredRows`. Explicit-model mode alone accepts `beforeRowChange`.
7171

‎apps/website/content/docs/grid/pretable-component.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ export function People({ rows }: { rows: Person[] }) {
3838
}
3939
```
4040

41-
`getRowId` is required — there is no positional default. Row identity is never derived from array position, so pass a function that reads a stable key off the row itself.
41+
Row identity is never derived from array position. `Person` above has a conventional `id`, so `getRowId` is optional there and the grid reads `row.id`; the example passes it anyway to name the key explicitly. For a row whose stable key is anything else, `getRowId` is required and omitting it is a type error, not a runtime surprise.
4242

4343
## Props
4444

45-
`columns`, `rows`, `ariaLabel`, and `getRowId` are required — `ariaLabel` is the grid's accessible name, so give it something a screen reader user would recognize as this table's contents (e.g. `"People directory"`, not a generic placeholder). The concise preset also forwards row activation and selection callbacks, row changes, clipboard hooks, tab behavior, messages, the optional selection column, and column width/order/pinning callbacks. Use [`<PretableSurface>`](/docs/grid/pretable-surface) for controlled query and interaction state, grouping controls, custom renderers, explicit row-model ownership, telemetry, or a custom viewport height.
45+
`columns`, `rows`, and `ariaLabel` are required, and so is `getRowId` unless the row has a conventional string or number `id` — `ariaLabel` is the grid's accessible name, so give it something a screen reader user would recognize as this table's contents (e.g. `"People directory"`, not a generic placeholder). The concise preset also forwards row activation and selection callbacks, row changes, clipboard hooks, tab behavior, messages, the optional selection column, and column width/order/pinning callbacks. Use [`<PretableSurface>`](/docs/grid/pretable-surface) for controlled query and interaction state, grouping controls, custom renderers, explicit row-model ownership, telemetry, or a custom viewport height.
4646

4747
## Defaults
4848

‎packages/react/react.api.md‎

Lines changed: 64 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,7 @@ export interface LabeledGridSurfaceBaseProps<TRow extends PretableRow = Pretable
216216
column: PretableColumn<TRow>;
217217
sortDirection: PretableSortDirection;
218218
}) => HTMLAttributes<HTMLButtonElement> | undefined;
219-
// (undocumented)
220-
getRowId: (row: TRow) => TRowId;
219+
getRowId?: (row: TRow) => TRowId;
221220
// (undocumented)
222221
headerCellClassName?: string;
223222
// (undocumented)
@@ -279,7 +278,7 @@ export interface LabeledGridSurfaceFormatValueInput<TRow extends PretableRow = P
279278
// @beta
280279
export type LabeledGridSurfaceProps<TRow extends PretableRow = PretableRow, TRowId extends PretableRowId = TRow extends {
281280
readonly id: infer TId extends PretableRowId;
282-
} ? TId : PretableRowId> = LabeledGridSurfaceBaseProps<TRow, TRowId> & ({
281+
} ? TId : PretableRowId> = LabeledGridSurfaceBaseProps<TRow, TRowId> & PretableRowIdRequirement<TRow, TRowId> & ({
283282
query: PretableQueryFor<PretableSurfaceQueryColumns<TRow>>;
284283
onQueryChange: (query: PretableQueryFor<PretableSurfaceQueryColumns<TRow>>) => void;
285284
} | {
@@ -330,7 +329,7 @@ export function Pretable<TRow extends PretableRow = PretableRow, TRowId extends
330329
readonly id: infer TId extends PretableRowId;
331330
} ? TId : PretableRowId, const TColumns extends readonly {
332331
readonly id: string;
333-
}[] = readonly PretableColumn<TRow>[]>(input: PretableProps<TRow, TRowId, TColumns>): react.JSX.Element;
332+
}[] = readonly PretableColumn<TRow>[]>(props: PretableProps<TRow, TRowId, TColumns>): react.JSX.Element;
334333

335334
// @public (undocumented)
336335
export interface PretableAggregateFormatInput<TValue, TColumn> {
@@ -383,6 +382,50 @@ export interface PretableBadgeProps extends Omit<HTMLAttributes<HTMLSpanElement>
383382
// @public
384383
export type PretableBadgeTone = "positive" | "negative" | "warning" | "info";
385384

385+
// @public
386+
export interface PretableBaseProps<TRow extends PretableRow = PretableRow, TRowId extends PretableRowId = TRow extends {
387+
readonly id: infer TId extends PretableRowId;
388+
} ? TId : PretableRowId, TColumns extends readonly {
389+
readonly id: string;
390+
}[] = readonly PretableColumn<TRow>[]> {
391+
// (undocumented)
392+
ariaLabel: string;
393+
columns: TColumns;
394+
// (undocumented)
395+
copyToClipboard?: PretableSurfaceProps<TRow, TRowId, TColumns>["copyToClipboard"];
396+
// (undocumented)
397+
copyWithHeaders?: PretableSurfaceProps<TRow, TRowId, TColumns>["copyWithHeaders"];
398+
dataState?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["dataState"];
399+
getRowId?: (row: TRow) => TRowId;
400+
// (undocumented)
401+
locale?: PretableSurfaceProps<TRow, TRowId, TColumns>["locale"];
402+
// (undocumented)
403+
messages?: PretableSurfaceProps<TRow, TRowId, TColumns>["messages"];
404+
// (undocumented)
405+
onColumnOrderChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnOrderChange"];
406+
// (undocumented)
407+
onColumnPinnedChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnPinnedChange"];
408+
// (undocumented)
409+
onColumnWidthsChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnWidthsChange"];
410+
// (undocumented)
411+
onCopy?: PretableSurfaceProps<TRow, TRowId, TColumns>["onCopy"];
412+
onQueryChange?: (query: PretableQueryFor<PretableSurfaceQueryColumns<TRow>>) => void;
413+
// (undocumented)
414+
onRowActivate?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowActivate"];
415+
// (undocumented)
416+
onRowChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowChange"];
417+
// (undocumented)
418+
onRowSelectionChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowSelectionChange"];
419+
processing?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["processing"];
420+
resultMeta?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["resultMeta"];
421+
// (undocumented)
422+
rows: readonly TRow[];
423+
// (undocumented)
424+
rowSelectionColumn?: PretableSurfaceProps<TRow, TRowId, TColumns>["rowSelectionColumn"];
425+
// (undocumented)
426+
tabBehavior?: PretableSurfaceProps<TRow, TRowId, TColumns>["tabBehavior"];
427+
}
428+
386429
// @public
387430
export type PretableBodyStateKind = "loading" | "empty" | "error" | "error-strip";
388431

@@ -1562,49 +1605,11 @@ export interface PretableProcessingOptions {
15621605
}
15631606

15641607
// @public
1565-
export interface PretableProps<TRow extends PretableRow = PretableRow, TRowId extends PretableRowId = TRow extends {
1608+
export type PretableProps<TRow extends PretableRow = PretableRow, TRowId extends PretableRowId = TRow extends {
15661609
readonly id: infer TId extends PretableRowId;
15671610
} ? TId : PretableRowId, TColumns extends readonly {
15681611
readonly id: string;
1569-
}[] = readonly PretableColumn<TRow>[]> {
1570-
// (undocumented)
1571-
ariaLabel: string;
1572-
columns: TColumns;
1573-
// (undocumented)
1574-
copyToClipboard?: PretableSurfaceProps<TRow, TRowId, TColumns>["copyToClipboard"];
1575-
// (undocumented)
1576-
copyWithHeaders?: PretableSurfaceProps<TRow, TRowId, TColumns>["copyWithHeaders"];
1577-
dataState?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["dataState"];
1578-
// (undocumented)
1579-
getRowId: (row: TRow) => TRowId;
1580-
// (undocumented)
1581-
locale?: PretableSurfaceProps<TRow, TRowId, TColumns>["locale"];
1582-
// (undocumented)
1583-
messages?: PretableSurfaceProps<TRow, TRowId, TColumns>["messages"];
1584-
// (undocumented)
1585-
onColumnOrderChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnOrderChange"];
1586-
// (undocumented)
1587-
onColumnPinnedChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnPinnedChange"];
1588-
// (undocumented)
1589-
onColumnWidthsChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onColumnWidthsChange"];
1590-
// (undocumented)
1591-
onCopy?: PretableSurfaceProps<TRow, TRowId, TColumns>["onCopy"];
1592-
onQueryChange?: (query: PretableQueryFor<PretableSurfaceQueryColumns<TRow>>) => void;
1593-
// (undocumented)
1594-
onRowActivate?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowActivate"];
1595-
// (undocumented)
1596-
onRowChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowChange"];
1597-
// (undocumented)
1598-
onRowSelectionChange?: PretableSurfaceProps<TRow, TRowId, TColumns>["onRowSelectionChange"];
1599-
processing?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["processing"];
1600-
resultMeta?: PretableSurfaceSharedProps<TRow, TRowId, TColumns>["resultMeta"];
1601-
// (undocumented)
1602-
rows: readonly TRow[];
1603-
// (undocumented)
1604-
rowSelectionColumn?: PretableSurfaceProps<TRow, TRowId, TColumns>["rowSelectionColumn"];
1605-
// (undocumented)
1606-
tabBehavior?: PretableSurfaceProps<TRow, TRowId, TColumns>["tabBehavior"];
1607-
}
1612+
}[] = readonly PretableColumn<TRow>[]> = PretableBaseProps<TRow, TRowId, TColumns> & PretableRowIdRequirement<TRow, TRowId>;
16081613

16091614
// @public (undocumented)
16101615
export interface PretableQueryFor<TColumns> {
@@ -1788,6 +1793,15 @@ export type PretableRowGroupFor<TColumns> = Prettify<(TColumns extends readonly
17881793
// @public (undocumented)
17891794
export type PretableRowId = string | number;
17901795

1796+
// @public
1797+
export type PretableRowIdRequirement<TRow, TRowId extends PretableRowId> = [
1798+
TRow
1799+
] extends [{
1800+
readonly id: PretableRowId;
1801+
}] ? unknown : {
1802+
readonly getRowId: (row: TRow) => TRowId;
1803+
};
1804+
17911805
// @public (undocumented)
17921806
export interface PretableRowModel<TRow extends object, TRowId extends PretableRowId, TColumns> {
17931807
// (undocumented)
@@ -2206,10 +2220,10 @@ export interface PretableSurfaceRowInput<TRow extends PretableRow = PretableRow,
22062220
// @public
22072221
export type PretableSurfaceRowsProps<TRow extends PretableRow, TRowId extends PretableRowId, TColumns extends readonly {
22082222
readonly id: string;
2209-
}[]> = PretableSurfaceSharedProps<TRow, TRowId, TColumns> & {
2223+
}[]> = PretableSurfaceSharedProps<TRow, TRowId, TColumns> & PretableRowIdRequirement<TRow, TRowId> & {
22102224
readonly rows: readonly TRow[];
22112225
readonly columns: TColumns;
2212-
readonly getRowId: (row: TRow) => TRowId;
2226+
readonly getRowId?: (row: TRow) => TRowId;
22132227
readonly model?: never;
22142228
readonly aggregateFilteredRows?: boolean;
22152229
readonly initialExpansion?: PretableExpansionDefault;
@@ -2575,9 +2589,11 @@ export interface UsePretableModelOptions<TModel> extends PretableViewportOptions
25752589
}
25762590

25772591
// @public
2578-
export type UsePretableRowsOptions<TColumns> = PretableRowsModeBaseOptions<PretableRowForColumns<TColumns>, PretableConventionalRowId<PretableRowForColumns<TColumns>>, TColumns> & {
2592+
export type UsePretableRowsOptions<TColumns> = PretableRowForColumns<TColumns> extends {
2593+
readonly id: PretableRowId;
2594+
} ? PretableRowsModeBaseOptions<PretableRowForColumns<TColumns>, PretableConventionalRowId<PretableRowForColumns<TColumns>>, TColumns> & {
25792595
readonly getRowId?: undefined;
2580-
} & PretableQueryOptions<TColumns>;
2596+
} & PretableQueryOptions<TColumns> : never;
25812597

25822598
// @public
25832599
export type UsePretableRowsWithIdOptions<TColumns, TRowId extends PretableRowId> = PretableRowsModeBaseOptions<PretableRowForColumns<TColumns>, TRowId, TColumns> & {

0 commit comments

Comments
 (0)