Skip to content

[DataGrid] Fix noRowsOverlay flicker between dataSource re-fetches#22465

Merged
LukasTy merged 8 commits into
mui:masterfrom
LukasTy:claude/blissful-ishizaka-570f57
May 27, 2026
Merged

[DataGrid] Fix noRowsOverlay flicker between dataSource re-fetches#22465
LukasTy merged 8 commits into
mui:masterfrom
LukasTy:claude/blissful-ishizaka-570f57

Conversation

@LukasTy

@LukasTy LukasTy commented May 15, 2026

Copy link
Copy Markdown
Member

Fixes #22458
Closes #22460.

Summary

Fixes the brief "No rows" overlay flash that appears between pagination, sort, filter, and row-grouping changes when using the dataSource API. The flash is especially visible on cached navigation, where the response is available synchronously but a stale empty render still appears for a frame.

Root cause

The navigation event handlers (handleFetchRowsOnParamsChange in useGridDataSourceBase and handleRowGroupingModelChange in useGridDataSourcePremium) called apiRef.current.setRows([]) synchronously and queued debouncedFetchRows() afterwards. Inside useGridRows, setRows rebuilds state.rows from props.loading via getRowsStateFromCache. Because dataSource grids don't pass a loading prop, that rebuild reset state.rows.loading to undefined, exposing a render with rows = [] and loading = false. The overlay selector then picked noRowsOverlay for that single frame.

Fix

In both navigation handlers, call setLoading(true) immediately after setRows([]) so the loading flag survives the setRows rebuild. The first render after the click now has rows = [] and loading = true, which picks loadingOverlay (skeleton variant) instead of noRowsOverlay. The cache-hit branch of fetchRows now also calls setLoading(false) so the loading flag the handler just set is cleared when the cached response is applied synchronously.

In the Premium row-grouping handler, the event listener is wired without a strategy gate, so the setLoading(true) is wrapped in an inline strategy check — fetchRows only clears the loading flag when a standard strategy is active, so we mustn't flip it on for lazy-loading or other non-standard strategies.

Follow-up

The original draft of this PR also added a dataSourceKeepPreviousData opt-in prop. Per the review on this PR, that interacts badly with the tree-data fix from #21619 (skipping the synchronous setRows([]) would leave sort/filter changes rendering rows in stale order). The prop is moved to a follow-up PR that handles the tree-data interaction properly.

Test plan

  • Added unit tests in dataSource.DataGrid.test.tsx covering cache-hit, cache-miss, and empty-response cases.
  • Added a Premium row-grouping test that exercises the loading toggle on grouping model change.
  • pnpm test:unit --project "x-data-grid*" --run clean.
  • pnpm typescript clean across the monorepo.
  • pnpm eslint, pnpm prettier, pnpm markdownlint clean.
  • Manual verification on the Data Source demo: clicking back and forth between pages no longer flashes the "No rows" overlay.

The navigation event handlers cleared rows synchronously before queueing the
fetch. Because `setRows` rebuilds `state.rows` from `props.loading`, the
synchronous clear also reset `loading` to false, exposing a render with
`rows = []` and `loading = false` that triggered `noRowsOverlay`. The flash
was most visible when navigating between already-cached pages.

Now the handlers call `setLoading(true)` after `setRows([])`, so the loading
state survives the `setRows` rebuild and the overlay selector picks
`loadingOverlay` instead. Add the `dataSourceKeepPreviousData` prop to opt
into keeping the previously displayed rows visible during the fetch
(mirrors React Query's `placeholderData: keepPreviousData`).

Fixes mui#22458

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@code-infra-dashboard

code-infra-dashboard Bot commented May 15, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-22465--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 🔺+66B(+0.02%) 🔺+9B(+0.01%)
@mui/x-data-grid-pro 🔺+66B(+0.01%) 🔺+12B(+0.01%)
@mui/x-data-grid-premium 🔺+178B(+0.03%) 🔺+36B(+0.02%)
@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-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


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

@LukasTy LukasTy added 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 May 15, 2026
@LukasTy LukasTy self-assigned this May 15, 2026
@LukasTy LukasTy changed the title [data-grid] Fix noRowsOverlay flicker between dataSource re-fetches [data grid] Fix noRowsOverlay flicker between dataSource re-fetches May 15, 2026
@LukasTy
LukasTy marked this pull request as ready for review May 15, 2026 14:45
…ousData`

The error path in `handleDataUpdate` and `handleGroupedDataUpdate` cleared
rows unconditionally, so a rejected refetch wiped the last known-good data
even when `dataSourceKeepPreviousData` was on — the opposite of what users
opt into the prop for. Gate both `setRows([])` calls behind the prop so a
failed refetch leaves the previous rows visible while `onDataSourceError`
delivers the error.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@GMchris

GMchris commented May 17, 2026

Copy link
Copy Markdown
Contributor

Hey Lukas, Im the original reporter of this issue. I figured I'll share my thinking here instead of my own PR since this one is more comprehensive anyway.

Im personally happy with this approach and would love to have any resolution honestly. Just two things to consider:

  1. By making this prop and defaulting it to false I'd argue you're maintaining behavior (thus not a breaking change -good) but making a worse overall default behavior. Do you really want to make the default behavior actually include this flash of no rows?
  2. I guess the ship may have sailed since this isnt the only "dataSource" related prop, but don't these props make more sense as part o the dataSource object? The one that accepts getRows, like so:
<DataGrid
dataSource={{
   fetchRows: ...,
  keepPreviousData: true
}}

The second is more subjective, but for the first I'll offer an alternative. You can keep the previous behavior (don't persist previous data) and get rid of the flash by removing the "debounce" on the fetchRows. Im guessing its there for a reason, maybe the original implementation didn't want to call setRows([]) and then setRows(someRows) immediately after, but removing this debounce, I can confirm, fixes the flash without needing to persist rows.

My ideal solution would be to do both. Remove debounce (if acceptable) to improve default behavior for everyone out of the box and add this new prop for people like me who prefer keeping previous rows mounted for aesthetic purposes.

@LukasTy

LukasTy commented May 18, 2026

Copy link
Copy Markdown
Member Author

@GMchris thank you for the response.
I agree with both points.
If other Data Grid maintainers agree, we could add it to the object config.

In regards to the No Rows flashing, isn't it fixed: https://deploy-preview-22465--material-ui-x.netlify.app/x/react-data-grid/server-side-data/#with-the-data-source? Or can you still reproduce it in this demo?

@siriwatknp siriwatknp 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.

👍 Thanks for taking care of this, here is what I found:

(1) setLoading(true) in handleRowGroupingModelChange looks asymmetric with the clear paths.
(2) Test misses state.rows.loading assertion.
(3) Guard on setRows([]) for non-standard strategies — is it intentional?

if (!props.dataSourceKeepPreviousData) {
apiRef.current.setRows([]);
}
apiRef.current.setLoading(true);

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.

(1) Is there a reason setLoading(true) is called unconditionally here?

From what I traced, the setLoading(false) paths in fetchRows (cache-hit at useGridDataSourceBase.ts:128-135 and finally at :182-187) only run when standardRowsUpdateStrategyActive.

Switching rowGroupingModel from ['a'] to ['b'] while the grouped strategy stays active sets loading true and never clears it — overlay stays on top of grouped rows.

The community handleFetchRowsOnParamsChange (useGridDataSourceBase.ts:377-389) guards both setRows([]) and setLoading(true) with the same standardRowsUpdateStrategyActive check. This handler is asymmetric.

Could wrap both calls in the same guard:

Suggested change
apiRef.current.setLoading(true);
if (standardRowsUpdateStrategyActive) {
setLoading(true);
}

Or if loading must be set for grouped mode, please add a clear path in the cache-hit branch and finally too.

Either way, please add a Premium grouping regression test.

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.

Addressed by adding an inline strategy check in useGridDataSourcePremium. The event listener is wired without a strategy gate (runIf(!pivotActive && !!props.dataSource, ...)), so the handler can fire while a non-standard strategy is active. fetchRows only clears the loading flag when a standard strategy is active, so setLoading(true) could otherwise stick.

const handleRowGroupingModelChange = React.useCallback(() => {
  // The event listener is wired regardless of the active strategy
  // (see `runIf(!pivotActive && !!props.dataSource, ...)` below), so we have to gate the
  // `setLoading(true)` here. `fetchRows` only clears the loading flag when a standard
  // strategy is active, and we don't want to leave the overlay stuck on lazy-loading or
  // other non-standard strategies.
  const currentStrategy = apiRef.current.getActiveStrategy(GridStrategyGroup.DataSource);
  const isStandardStrategyActive =
    currentStrategy === DataSourceRowsUpdateStrategy.Default ||
    currentStrategy === DataSourceRowsUpdateStrategy.GroupedData;
  if (isStandardStrategyActive) {
    if (!props.dataSourceKeepPreviousData) {
      apiRef.current.setRows([]);
    }
    apiRef.current.setLoading(true);
  }
  stopPolling();
  debouncedFetchRows();
}, [apiRef, props.dataSourceKeepPreviousData, debouncedFetchRows, stopPolling]);

Also added two regression tests in dataSourceRowGrouping.DataGridPremium.test.tsx covering the loading-state toggle on grouping model change and the keep-previous-data behavior.

Comment thread packages/x-data-grid/src/tests/dataSource.DataGrid.test.tsx Outdated
LukasTy and others added 2 commits May 20, 2026 15:35
…mmetry

The community handler had a redundant `if (standardRowsUpdateStrategyActive)`
guard inside the callback even though the event listener is already wired
with `runIf(standardRowsUpdateStrategyActive, ...)`. Drop the inner guard
and add a comment so the gating intent is explicit. This also restores the
pre-PR behavior of unconditionally clearing rows on non-standard strategies
(the outer `runIf` skips the handler entirely instead).

The Premium row-grouping handler is wired without a strategy check at the
event level, so it could call `setLoading(true)` while `fetchRows` would
not call the matching `setLoading(false)` (cleared only on standard
strategies). Add an inline strategy check there so the loading flag is
only flipped when `fetchRows` will eventually clear it again.

Tests:
- Add `state.rows.loading === false` assertion to the error-recovery test.
- Add Premium row-grouping regression tests that exercise the loading
  toggle and the keep-previous-data behavior on grouping model change.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@LukasTy
LukasTy requested a review from siriwatknp May 21, 2026 09:49
### Keep previous data while fetching

By default, the Data Grid clears the visible rows and shows a loading overlay while it fetches new data after pagination, sorting, filtering, or row grouping changes.
Pass the `dataSourceKeepPreviousData` prop to keep the previously displayed rows visible until the next response arrives.

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.

I assume the core problem (no rows flicker) would solved by setting the loading state after resetting the rows. Is that right?

In that case, I'd suggest to skip this prop for now or at least take this separately in order to make sure it doesn't causes issues like #21619 with nested data anymore.

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.

Indeed. Fixing the actual issue is fairly trivial and minimal.
The suggestion to split efforts makes a lot of sense. 👌

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.

@MBilalShafi, I have updated the solution in this PR to only fix the root issue.

@MBilalShafi MBilalShafi changed the title [data grid] Fix noRowsOverlay flicker between dataSource re-fetches [DataGrid] Fix noRowsOverlay flicker between dataSource re-fetches May 21, 2026
@MBilalShafi MBilalShafi added the feature: Server integration Better integration with backends, e.g. data source label May 21, 2026
Per review on mui#22465 (mui#22465 (comment)),
the opt-in prop has the same shape as the bug mui#21619 fixed: skipping the
synchronous `setRows([])` would leave tree-data sort/filter changes rendering
rows in stale order until the user collapsed/re-expanded. Keep this PR focused
on the reported `noRowsOverlay` flicker and ship the controllable prop in a
follow-up that handles the tree-data interaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@LukasTy LukasTy added type: bug It doesn't behave as expected. and removed 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 May 22, 2026
@LukasTy
LukasTy requested a review from Copilot May 22, 2026 07:52

Copilot AI left a comment

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.

Pull request overview

Fixes a transient noRowsOverlay flash when using the DataGrid dataSource API by ensuring the grid remains in a loading state while rows are temporarily cleared during query-parameter changes (pagination/sort/filter/row-grouping), including cache-hit scenarios.

Changes:

  • Set loading=true immediately after setRows([]) on params changes to prevent a one-frame rows=[] && loading=false state.
  • Clear the loading flag on cache-hit updates so synchronous cached responses don’t leave the grid stuck in a loading state.
  • Add unit tests covering cache-hit, cache-miss, and empty-response overlay behavior, plus a Premium row-grouping loading-state test.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/x-data-grid/src/hooks/features/dataSource/useGridDataSourceBase.ts Adjusts loading state transitions around setRows([]) and cache-hit response application.
packages/x-data-grid-premium/src/hooks/features/dataSource/useGridDataSourcePremium.tsx Updates row-grouping change handler to conditionally toggle loading based on active strategy.
packages/x-data-grid/src/tests/dataSource.DataGrid.test.tsx Adds regression tests to ensure noRowsOverlay does not render between re-fetches (cache hit/miss) and still renders for empty results.
packages/x-data-grid-premium/src/tests/dataSourceRowGrouping.DataGridPremium.test.tsx Adds a test validating loading state toggling during row-grouping model changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Two fixes flagged in mui#22465 (review):

1. The cache-hit early-return in `fetchRows` did not bump `lastRequestId`,
   so a previously started cache-miss request could resolve after a cache-hit
   navigation and override the cached data with stale rows. Bump
   `lastRequestId.current` in the cache-hit branch so any in-flight request
   is treated as stale when it resolves. Add a regression test that asserts
   a stale page-1 response does not override the cached page-0 data.

2. `handleRowGroupingModelChange` in the Premium hook now wraps both
   `setRows([])` and `setLoading(true)` in the `isStandardStrategyActive`
   block — the PR description only intended to gate the loading toggle.
   Restore the pre-PR unconditional `setRows([])` so non-standard strategies
   (lazy loading) still reset their grouping state on rowGroupingModel
   change.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@LukasTy
LukasTy requested review from MBilalShafi and Copilot May 22, 2026 12:42

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

…w-grouping test

Per mui#22465 (review),
`resolveSecond(...)` triggers React state updates (the awaiting `fetchRows`
completes and calls `setRows` + `setLoading(false)`) while the grid is
mounted, so it has to be wrapped in `act()` to avoid "not wrapped in act"
warnings under vitest-fail-on-console.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@LukasTy
LukasTy enabled auto-merge (squash) May 27, 2026 09:00
@LukasTy
LukasTy merged commit 6aab9e3 into mui:master May 27, 2026
21 checks passed
@LukasTy
LukasTy deleted the claude/blissful-ishizaka-570f57 branch May 27, 2026 09:30
mbrookes pushed a commit to mbrookes/mui-x that referenced this pull request Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: Server integration Better integration with backends, e.g. data source scope: data grid Changes related to the data grid. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Previous cache entry not persisted during dataSource.fetchRows calls resulting in no rows flicker

6 participants