What you were expecting:
A list page with 100 rows renders without errors under React 19.
What happened instead:
The first load of a list page with perPage={100} throws Maximum update depth exceeded one to three times. The page recovers, and every row ends up with its links, but each error is uncaught and reaches error monitoring. The error also occurs without <React.StrictMode>.
During one such load, the react-query cache holds about 250 ['auth', 'canAccess', …] queries: several per row, for rowClick and for the link of each <ReferenceField>.
Cause:
useCanAccess keys its query on recordId, so every row owns a distinct query, and every authProvider.canAccess() promise resolves on its own. react-query then flushes one observer notification per query, each in its own setTimeout(0) task, and React commits each of them separately.
React 19 counts a commit as a nested update when it leaves a Sync or Default lane pending. React 18 counted only a pending Sync lane (includesSomeLane(remainingLanes, SyncLane)). While an update scheduled by an earlier commit is still pending, these per-query commits add up and pass the limit of 50.
This is the mechanism that #11329 fixed for useGetManyAggregate, and #11379 for useFormGroup. useCanAccess is not covered yet.
Steps to reproduce:
- Use an
authProvider whose canAccess is async. Ours awaits a cached current-user promise, then checks the user's roles.
- Render a list with
perPage={100}, rowClick, and a few <ReferenceField> columns.
- With React 19.3, load the list page for the first time, so that no access check is cached.
I have not reduced this to a sandbox yet.
Suggested fix:
Apply the batching of resolveCallsWithData to useCanAccess: the checks that settle in the same tick write their result with setQueryData inside one notifyManager.batch, then their promises resolve. We run this change as a local patch on ra-core 5.15.4. With it, both pages we measured load without the error, and every canAccess query ends in status: 'success', fetchStatus: 'idle'. I will open a pull request with it.
Other information:
At the time of the error, the stack runs from the react-query notifyManager flush to the useSyncExternalStore store change, then forceStoreRerender.
Environment
- React-admin version: 5.15.4
- Last version that did not exhibit the issue (if applicable): none known. We hit this while moving from React 18.3.1 to 19.3.0, and did not test this page on React 18.
- React version: 19.3.0, with @tanstack/react-query 5.102.8
- Browser: Chrome
- Stack trace (in case of a JS error):
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
What you were expecting:
A list page with 100 rows renders without errors under React 19.
What happened instead:
The first load of a list page with
perPage={100}throwsMaximum update depth exceededone to three times. The page recovers, and every row ends up with its links, but each error is uncaught and reaches error monitoring. The error also occurs without<React.StrictMode>.During one such load, the react-query cache holds about 250
['auth', 'canAccess', …]queries: several per row, forrowClickand for the link of each<ReferenceField>.Cause:
useCanAccesskeys its query onrecordId, so every row owns a distinct query, and everyauthProvider.canAccess()promise resolves on its own. react-query then flushes one observer notification per query, each in its ownsetTimeout(0)task, and React commits each of them separately.React 19 counts a commit as a nested update when it leaves a Sync or Default lane pending. React 18 counted only a pending Sync lane (
includesSomeLane(remainingLanes, SyncLane)). While an update scheduled by an earlier commit is still pending, these per-query commits add up and pass the limit of 50.This is the mechanism that #11329 fixed for
useGetManyAggregate, and #11379 foruseFormGroup.useCanAccessis not covered yet.Steps to reproduce:
authProviderwhosecanAccessis async. Ours awaits a cached current-user promise, then checks the user's roles.perPage={100},rowClick, and a few<ReferenceField>columns.I have not reduced this to a sandbox yet.
Suggested fix:
Apply the batching of
resolveCallsWithDatatouseCanAccess: the checks that settle in the same tick write their result withsetQueryDatainside onenotifyManager.batch, then their promises resolve. We run this change as a local patch on ra-core 5.15.4. With it, both pages we measured load without the error, and everycanAccessquery ends instatus: 'success',fetchStatus: 'idle'. I will open a pull request with it.Other information:
At the time of the error, the stack runs from the react-query
notifyManagerflush to theuseSyncExternalStorestore change, thenforceStoreRerender.Environment
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.