Skip to content

[DataGrid] Expose apiRef on GridCallbackDetails for selector access in callbacks - #22973

Merged
michelengelen merged 22 commits into
mui:masterfrom
michelengelen:recipe/datagrid/20808
Aug 20, 2026
Merged

michelengelen merged 22 commits into
mui:masterfrom
michelengelen:recipe/datagrid/20808

Conversation

@michelengelen

@michelengelen michelengelen commented Jun 26, 2026 •

Copy link
Copy Markdown
Member

Adds apiRef to GridCallbackDetails. State selectors can now read the grid state directly inside event callbacks (for example onRowSelectionModelChange or onFilterModelChange). A separate useGridApiRef is not necessary.

  • publishEvent populates GridCallbackDetails.apiRef for all grid variants (Community, Pro, and Premium)
  • The "controlled state" callbacks that bypass publishEvent also receive it (onRowSelectionModelChange, onFilterModelChange, onSortModelChange, onPaginationModelChange, onDetailPanelExpandedRowIdsChange, onRowGroupingModelChange, onAggregationModelChange, onCellModesModelChange, onRowModesModelChange)
  • apiRef is a required field on GridCallbackDetails and matches its documented usage
  • details.apiRef is one stable, read-only ref for each grid instance. publishEvent does not allocate on the hot path. The type is Readonly<RefObject<Api>>, so an assignment to current is a compile error
  • The props interfaces, the processed props chains, and GridEventListener carry a defaulted Api type parameter. Consumers never write it. Every callback on every tier exposes a correctly typed apiRef, including the community-declared callbacks on Pro and Premium grids
  • The shared feature hooks reference the props with Api = any. This keeps the tier-boundary variance concession in internal signatures. The feature code contains no casts
  • New exported type aliases: GridCallbackDetailsPro and GridCallbackDetailsPremium
  • The existing api property stays for now (marked for deprecation in v10)
  • Adds a docs demo under "Access the state → In event handlers". The demo reads the row selection inside onFilterModelChange

Resolves #20808

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW

…allback

Signed-off-by: michel <jsnerdic@gmail.com>
Signed-off-by: michel <jsnerdic@gmail.com>
@michelengelen michelengelen self-assigned this Jun 26, 2026
@michelengelen michelengelen added docs Improvements or additions to the documentation. scope: data grid Changes related to the data grid. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. labels Jun 26, 2026
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 🔺+135B(+0.03%) 🔺+56B(+0.05%)
@mui/x-data-grid-pro 🔺+135B(+0.02%) 🔺+55B(+0.03%)
@mui/x-data-grid-premium 🔺+135B(+0.02%) 🔺+56B(+0.03%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 0B(0.00%) 0B(0.00%)
@mui/x-scheduler-premium 0B(0.00%) 0B(0.00%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

Signed-off-by: michel <jsnerdic@gmail.com>
@MBilalShafi

Copy link
Copy Markdown
Member

This method should work as a workaround, although I’m not sure it’s something we should document.

An alternative could be to expose apiRef through GridCallbackDetails, allowing users to access it directly. If we decide to go in that direction, we could deprecate the api argument and remove it in the next major release, then documenting it will be closer to the already familiar selector() pattern.

Also, I think this would fit better under the https://mui.com/x/react-data-grid/state/#access-the-state section rather than the selection docs, since it applies to all event handlers that receive callback arguments, not just selection related ones.

Signed-off-by: michel <jsnerdic@gmail.com>
@michelengelen

Copy link
Copy Markdown
Member Author

This method should work as a workaround, although I’m not sure it’s something we should document.

An alternative could be to expose apiRef through GridCallbackDetails, allowing users to access it directly. If we decide to go in that direction, we could deprecate the api argument and remove it in the next major release, then documenting it will be closer to the already familiar selector() pattern.

Also, I think this would fit better under the https://mui.com/x/react-data-grid/state/#access-the-state section rather than the selection docs, since it applies to all event handlers that receive callback arguments, not just selection related ones.

Thanks for the review!
The content is already placed in state.md under "Access the state", not in the selection docs. So that's already addressed.

I agree with exposing apiRef directly on GridCallbackDetails. Since publicApiRef is already a RefObject<GridApiCommon> internally, this would be a small additive change:

// GridCallbackDetails
apiRef: React.RefObject<GridApiCommon>;

And in useGridApiInitialization.ts, populate it unconditionally (not just for Pro/Premium like api currently is). That would also make the feature work with the community DataGrid.

The demo would then simplify to:

const updateSelectedDesks = (details: GridCallbackDetails) => {
  const selectedRows = gridRowSelectionIdsSelector(details.apiRef);
  ...
};

I'll update this PR to add apiRef to GridCallbackDetails and rework the demo accordingly. That way we're documenting a first-class API rather than a workaround. Deprecating api can be a follow-up if desired.

@michelengelen michelengelen changed the title [docs] Add recipe for reading grid state from event callbacks [data-grid] Expose apiRef on GridCallbackDetails for selector access in callbacks Jun 29, 2026
@michelengelen michelengelen changed the title [data-grid] Expose apiRef on GridCallbackDetails for selector access in callbacks [DataGrid] Expose apiRef on GridCallbackDetails for selector access in callbacks Jun 29, 2026
@michelengelen michelengelen removed the docs Improvements or additions to the documentation. label Jun 29, 2026
@zannager
zannager removed the request for review from mj12albert July 7, 2026 11:10

@LukasTy LukasTy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: right direction and the runtime plumbing is complete (every control-state callback goes through the single propOnChange call site you patched, so nothing is left unpopulated), but the type is too narrow for Pro/Premium, the docs section needs a controlled-mode caveat, and there are no tests.

  • apiRef: RefObject<GridApiCommon> means Pro/Premium selectors do not compile through it -- which is the exact use case being documented. Both of these fail tsgo with TS2345 (GridStateCommunity is missing columnReorder, detailPanel, dataSource / rowGrouping, aggregation, cellSelection, pivoting, ...):

    <DataGridPro onDetailPanelExpandedRowIdsChange={(ids, details) => gridDetailPanelExpandedRowIdsSelector(details.apiRef)} />
    <DataGridPremium onRowGroupingModelChange={(model, details) => gridRowGroupingSanitizedModelSelector(details.apiRef)} />
    

    GridCallbackDetails already takes one generic (K); adding Api extends GridApiCommon = GridApiCommon and threading GridApiPro/GridApiPremium through dataGridProProps.ts / dataGridPremiumProps.ts fixes it. The demo only compiles because it happens to use a community selector.

  • In controlled mode the selector returns stale state, and the new docs section does not mention it. useGridStateInitialization.setState sets ignoreSetState and skips the state commit when the prop wins, then calls propOnChange. Verified: with sortModel controlled, onSortModelChange receives model = [{ field: 'brand', sort: 'asc' }] while gridSortModelSelector(details.apiRef) returns []; same divergence for rowSelectionModel. Uncontrolled is correct. The page already carries a warning about controlled models under "Save and restore the state" -- this section needs a matching :::warning.

  • The demo uses DataGridPro for a community-only feature. Every other demo in docs/data/data-grid/state/ uses DataGrid from @mui/x-data-grid, and both gridRowSelectionIdsSelector and details.apiRef are community. Nothing in it needs Pro.

  • No tests. apiRef is now a required public field populated from three separate paths (publishEvent, propOnChange, the two editing hooks) and none of them is covered.

  • useGridStateInitialization.ts:94 inlines { current: apiRef.current.getPublicApi() } instead of the getPublicApiRef(apiRef) helper this same PR uses in both editing hooks. Related: publishEvent passes the grid's own internal publicApiRef, so user code assigning details.apiRef.current would corrupt the grid -- using getPublicApiRef in all three places is both consistent and safe. Also, the object spread in publishEvent doubles the per-event allocation on the grid's hottest path; a plain conditional assignment avoids it.

…des consistent and type-safe API access in event handlers

Signed-off-by: michel <jsnerdic@gmail.com>
michelengelen and others added 2 commits August 7, 2026 14:51
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW
- `GridControlStateItem` and `GridStatePrivateApi` take an `Api` generic
  (defaulted to `GridApiCommon`), instantiated with the package-specific
  public API through `GridPrivateOnlyApiCommon`, so Pro/Premium
  `registerControlState` accepts the narrowed `propOnChange` handlers
  natively and the `as` casts are gone
- add exported `GridCallbackDetailsPro`/`GridCallbackDetailsPremium`
  aliases and use them in the prop declarations
- make `getPublicApiRef` return a per-instance cached, getter-based ref:
  no allocation on the `publishEvent` hot path and the ref cannot be used
  to re-point the grid's internal ref
- pin the stable-ref behavior and the controlled-model staleness caveat
  with tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW
@michelengelen

Copy link
Copy Markdown
Member Author

@LukasTy I hardened the types, threaded the apiRef correctly through the state mechanism and added some tests for each of the packages (community, pro and premium). Could you please re-review it in this state? Thanks!

newRowSelectionModel: GridRowSelectionModel,
details: GridCallbackDetails,
) => {
updateSelectedDesks(details);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should find some other example to use this.
You could use the selector outside of the callback to react to the selection change and show the selected desks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this would not show how the selector can be accessed from inside the callback. Passing in a valid apiRef reference into the callback parameters is the whole purpose of this PR, so your suggestion would not showcase that.
Or did I misunderstand something here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about this?

const handleFilterModelChange = (
  model: GridFilterModel,
  details: GridCallbackDetails<'filter'>,
) => {
  // The row selection is not part of the callback arguments,
  // but it can be read from the state through `details.apiRef`
  const selectedRows = gridRowSelectionIdsSelector(details.apiRef);
  const desks = [...selectedRows.values()].map((row) => row?.desk).filter(Boolean);

  if (selectedRows.size === 0) {
    setMessage('Filter changed with no rows selected');
  } else {
    setMessage(
      `Filter changed with ${selectedRows.size} selected row(s): ${desks.join(', ')}`,
    );
  }
};

This would set a state value based on the results from a selector inside the callback

@arminmeh arminmeh Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that this is not a "good" reason to use selectors in the callback.
You use the selector to update the root state, which defies the whole purpose of the selectors and makes the whole grid re-render because of the inline props.

@LukasTy

LukasTy commented Aug 12, 2026

Copy link
Copy Markdown
Member

Claude Opus 5 review

Verdict: most of the previous round is properly addressed -- generics on GridCallbackDetails, the controlled-model warning plus a regression test, the demo moved to community DataGrid, getPublicApiRef used in all three paths and now cached per grid instance with a getter-based current. One issue remains: the type threading is only half applied.

  • Api is threaded only through the Pro/Premium-declared props, so most callbacks on a Premium grid still hand out a community-typed apiRef. Verified with tsgo: gridRowGroupingSanitizedModelSelector(details.apiRef) compiles in onAggregationModelChange / onRowGroupingModelChange / onCellSelectionModelChange, and fails with TS2345 in all three of these on the same <DataGridPremium>:

    onFilterModelChange={(model, details) => ...}      // control-state prop declared in DataGridProps
    onColumnHeaderClick={(params, event, details) => ...} // any GridEventListener-based callback
    onCellModesModelChange={(model, details) => ...}   // editing hook path
    

    onFilterModelChange is one of the two callbacks the new docs section names, so the documented pattern still needs a cast on Pro/Premium. Runtime is correct -- the real Premium API is passed -- but within a single grid some callbacks are typed and some are not, with nothing signalling which. The Pro test already encodes the split (RefObject<GridApiPro> for the detail panel, RefObject<GridApiCommon> for onCellModesModelChange). Two consistent ways out: thread Api through GridEventListener and the community props interfaces, or drop the GridCallbackDetailsPro/GridCallbackDetailsPremium aliases and document the cast. Either is better than the current half-state.

  • getPublicApiRef makes current a getter that throws on assignment, but the type still declares it writable. details.apiRef.current = x typechecks and only fails at runtime, which the new test enshrines as a TypeError. Typing the return as Readonly<RefObject<...>> rejects it at compile time and still satisfies the selector signature -- verified: gridFilterModelSelector(ro) compiles, ro.current = x gives TS2540.

props.signature === GridSignature.DataGridPro ||
props.signature === GridSignature.DataGridPremium
? { api: privateApiRef.current.getPublicApi() }
: {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was there a reason we didn't provide this for the community version before?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly don't know... @cherniavskii maybe?

* The reason for this callback to have been called.
*/
reason?: GridControlledStateReasonLookup[K];
// TODO: deprecate in v10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can deprecate now and remove in v10

newRowSelectionModel: GridRowSelectionModel,
details: GridCallbackDetails,
) => {
updateSelectedDesks(details);

@arminmeh arminmeh Aug 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that this is not a "good" reason to use selectors in the callback.
You use the selector to update the root state, which defies the whole purpose of the selectors and makes the whole grid re-render because of the inline props.

michelengelen and others added 2 commits August 14, 2026 13:46
Addresses review feedback: reactive display of the selection is what
selectors outside callbacks are for; the demo now shows reading state
that is not part of the callback arguments at event time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW
Addresses the remaining review findings:

- `DataGridPropsWithoutDefaultValue`, `DataGridProPropsWithoutDefaultValue`,
  the processed props chains, and `GridEventListener` are generic over the
  public API type (defaulted, never written by consumers), so every callback
  on every tier now exposes a correctly typed `details.apiRef` - including
  the community-declared callbacks on Pro/Premium grids and the editing
  model callbacks
- shared feature hooks and shared private-api surfaces reference the props
  with `Api = any`, concentrating the tier-boundary variance concession in
  internal signatures instead of per-feature casts; `useGridEvent` accepts
  `GridEventListener<E, Api>` and bridges to the common event system in one
  place
- `GridCallbackDetails.apiRef` is `Readonly<RefObject<Api>>`, so reassigning
  `current` is rejected at compile time, matching the getter-based runtime
  behavior
- tests: Pro editing-path callback now asserts a Pro-typed ref, Premium
  gains coverage for Premium selectors in community-inherited callbacks
  (control-state, event-based, and editing paths) and a compile-time
  read-only guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW
@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@michelengelen

Copy link
Copy Markdown
Member Author

Both findings from the review are now addressed (6f9b1c5).

Type threading is now complete

  • DataGridPropsWithoutDefaultValue, DataGridProPropsWithoutDefaultValue, the processed props chains, and GridEventListener carry an Api type parameter. The parameter has a default value for each tier. Consumers never write it.
  • Every callback on every tier now exposes a correctly typed details.apiRef. This includes the three cases that failed before: onFilterModelChange, onColumnHeaderClick, and onCellModesModelChange.
  • The shared feature hooks reference the props with Api = any. This concentrates the variance concession in internal signatures. useGridEvent bridges the typed handlers to the common event system in one commented location. The feature code contains no casts.
  • One inherited wart is also gone: Premium previously received a Pro-typed apiRef on onDetailPanelExpandedRowIdsChange. It is now Premium-typed.

The ref is now read-only at compile time

  • GridCallbackDetails.apiRef is Readonly<RefObject<Api>>. An assignment to current is now a TS2540 error.
  • The property declaration carries the modifier, not only the return type of getPublicApiRef. TypeScript ignores readonly modifiers in assignability checks, so the return type alone was not sufficient.

Tests

  • The Premium suite pins the three previously failing callbacks as positive tests, at compile time and at runtime.
  • The Pro editing-path test now asserts a Pro-typed ref. It asserted GridApiCommon before.
  • A @ts-expect-error guard pins the read-only behavior.

🤖 Generated with Claude Code

michelengelen and others added 2 commits August 14, 2026 14:45
…0808

# Conflicts:
#	packages/x-data-grid/src/hooks/features/columns/gridColumnsUtils.ts
The six Premium-declared event props kept the default common API type.
They now expose a Premium-typed `details.apiRef` like every other callback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dw4bjYohMj5g5QuALoFXoW
@github-actions github-actions Bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged. label Aug 14, 2026

@LukasTy LukasTy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, looks awesome. 👏


Final Claude review and cleanup suggestion

Verdict: mergeable. The remaining round-2 gap is closed -- Api is threaded through GridEventListener and the whole props chain, apiRef is Readonly<RefObject<Api>>, and Premium selectors now compile on every callback kind (control-state, event-based, and editing). Nothing here blocks merge. Two quality items, the first worth doing before merge because it removes 61 files from the diff.

  • The DataGridProcessedProps<any, any> churn is avoidable with a two-line change. 61 files and 77 occurrences exist only to spell the variance concession at every call site; 53 of the 75 touched package files contain no other change. Set the Api default to any on DataGridProcessedProps and DataGridProProcessedProps instead of GridApiCommunity/GridApiPro, and every bare DataGridProcessedProps in the internals already means the permissive form. Verified by reverting all 77 occurrences and flipping both defaults: x-data-grid, -pro, -premium and docs all typecheck clean, Premium selectors still compile in onFilterModelChange / onColumnHeaderClick / onCellModesModelChange / onAggregationModelChange / onRowClick, community typing is unchanged, and a legacy GridEventListener<'cellClick'> handler still assigns on all three tiers. Same concession the PR body describes, stated once instead of 61 times, and a much smaller backport and merge-conflict surface.

  • Two type-only assertions sit in a .test.tsx where the callbacks never fire. vitest.shared.mts:128 excludes .spec.{js,ts,tsx} from the runner, so type checks belong in a .spec.tsx. Verified by planting throw new Error(...) inside both callbacks in gridCallbackDetailsApiRef.DataGridPremium.test.tsx: all 3 tests still pass. should type the apiRef as read-only asserts nothing at runtime, and the onCellModesModelChange block in should accept Premium selectors on community-inherited callbacks is dead -- nothing in that test starts cell edit mode. Move both to a .spec.tsx, or trigger the callbacks.

michelengelen and others added 2 commits August 20, 2026 09:06
The internals are shared across the three packages.
A bare ProcessedProps reference now accepts props typed with any API type.
This removes the DataGridProcessedProps<any, any> spelling from every call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gpgjw7HVWvJj8vxoepBv1n
The test runner excludes .spec files, so type checks belong there.
The read-only check and the editing callback check assert nothing at runtime.
The runtime test now only keeps callbacks that fire.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gpgjw7HVWvJj8vxoepBv1n
@michelengelen

Copy link
Copy Markdown
Member Author

I addressed both points from the review:

  • The Api parameter of DataGridProcessedProps and DataGridProProcessedProps now defaults to any. All 77 <any, any> call sites went back to the bare form. The PR diff went from 75 to 26 files.
  • The two type-only assertions moved to DataGridPremium.spec.tsx. The runtime test now only keeps callbacks that fire.

Typecheck passes for the three grid packages and the docs. The remaining unit tests pass.

@michelengelen
michelengelen merged commit 20cb519 into mui:master Aug 20, 2026
22 checks passed
@michelengelen
michelengelen deleted the recipe/datagrid/20808 branch August 20, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: data grid Changes related to the data grid. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[docs][data grid] Accessing row selection from inside another callback (selection state is not managed)

5 participants