Skip to content

Fix "Maximum update depth exceeded" when many useCanAccess checks resolve at once - #11393

Open
paour wants to merge 3 commits into
marmelab:masterfrom
paour:fix-use-can-access-max-update-depth
Open

paour wants to merge 3 commits into
marmelab:masterfrom
paour:fix-use-can-access-max-update-depth

Conversation

@paour

@paour paour commented Oct 2, 2026

Copy link
Copy Markdown

Problem

Fixes #11392.

On React 19, the first load of a list page with many rows throws Maximum update depth exceeded when the authProvider implements canAccess:

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.

useCanAccess keys its query on recordId, so every row owns a distinct query (the rowClick link, the link of each <ReferenceField>, and so on), and every authProvider.canAccess() call 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 nested when it leaves a Sync or Default lane pending, where React 18 counted only a pending Sync lane, so these commits pass the limit of 50.

Measured on an app with perPage={100}: about 250 canAccess queries on one list page, and one to three uncaught errors per first load, with or without <StrictMode>. The page recovers, but every error reaches error monitoring.

Solution

The same approach as #11329 for useGetManyAggregate. The checks that settle in the same tick are collected, their result is written with queryClient.setQueryData inside a single notifyManager.batch, and then their promises resolve to let their query leave the fetching state. All the consumers get their result in one commit.

Checks whose query was canceled or removed while in flight are skipped, so their cache entry is not resurrected. Errors keep their current path. There is no public API change. A result reaches its consumers one setTimeout(0) later than before.

How To Test

Two tests added to useCanAccess.spec.tsx:

  • 30 consumers with distinct records get their result in a single React commit. Before the fix: Expected: 1, Received: 30.
  • A check canceled while in flight is not written back to the cache. Without the guard: Received: true.

The overflow itself needs React 19, and this repository tests on React 18. It was verified in an app on React 19.3.0 with this change applied to ra-core 5.15.4 as a patch: two list pages of 100 rows went from one to three uncaught errors per first load to none, and every canAccess query ended in status: 'success', fetchStatus: 'idle'.

The 21 spec files that exercise canAccess pass, as do the full ra-core and ra-ui-materialui suites.

Additional Checks

  • The PR targets master for a bugfix or a documentation fix, or next for a feature
  • The PR includes unit tests (if not possible, describe why)
  • The PR includes one or several stories (if not possible, describe why): the change has no visible effect in a story, and the overflow needs React 19.
  • The documentation is up to date: no public API change.

Also, please make sure to read the contributing guidelines.

…olve at once

useCanAccess owns one query per record, and each authProvider.canAccess()
call resolves on its own, so React commits each consumer separately. React 19
counts those commits as nested updates and throws past 50 of them, e.g. on a
list with a link per row.

Write the result of the checks that settle in the same tick inside a single
notifyManager transaction, as useGetManyAggregate does since marmelab#11329.

Fixes marmelab#11392

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.

Copilot review overview

🟡 Changes recommended

A canceled request can write stale authorization data if its query key is refetched before the old provider promise settles.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Batches concurrent useCanAccess results to prevent excessive React 19 commits.

Changes:

  • Batches successful authorization results through React Query.
  • Avoids restoring canceled queries.
  • Adds batching and cancellation tests.
File Description
packages/​ra-core/​src/​auth/​useCanAccess.ts Implements batched access-check resolution.
packages/​ra-core/​src/​auth/​useCanAccess.spec.tsx Tests commit batching and canceled-query handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/ra-core/src/auth/useCanAccess.ts Outdated
The batch looked a query up by key, so a check canceled and then
refetched before its authProvider call settled wrote its stale
result into the new fetch. Each check now carries the AbortSignal
of its own fetch, which react-query aborts on cancel and removal.
Comment thread packages/ra-core/src/auth/useCanAccess.ts Outdated
Comment thread packages/ra-core/src/auth/useCanAccess.spec.tsx Outdated
A setTimeout(0) delayed every check by a task. react-query already
delivers the notifications on its own setTimeout(0), so a microtask
is enough to share one notifyManager batch. The cancel-then-refetch
test now awaits the refetch.
@paour
paour requested a review from fzaninotto October 2, 2026 16:48

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useCanAccess throws Maximum update depth exceeded on long lists under React 19

3 participants