payloadcms / payloadcms/payload
`WhereBuilder/Condition/Select`: "Maximum update depth exceeded" when filter value/operator multiplicity mismatch (sibling to already-fixed #11080 Relationship filter bug)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx contains a self-correcting useEffect that can enter an infinite render loop, crashing the list view with:
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.
We caught this once in production via Sentry (a real user session, single occurrence) on Payload 3.85.1, then traced it to source. The compiled component (React Compiler output, but the logic is unchanged from source) contains:
// WhereBuilder/Condition/Select/index.js
React.useEffect(() => {
if (!isMulti && Array.isArray(value)) {
onChange(value[0])
}
}, [isMulti, onChange, value])
If value is an array while isMulti is false (i.e. the condition's operator no longer expects an array, but the value hasn't been normalized to a scalar yet), this effect calls onChange(value[0]). If the parent's updateCondition handler round-trips back a value that is still array-shaped on the next render (e.g. due to how the condition's operator/value pair gets serialized to/from the URL query string, or a race between the parent's own value-reset logic and this effect), the effect re-fires indefinitely.
This is the same class of bug that was already fixed in the sibling WhereBuilder/Condition/Relationship component via #11080 ("fix(ui): relationship filter renders stale values when changing fields"), which explicitly called out:
The
RelationshipFiltercomponent was previously relying on auseEffectthat had a callback in its dependencies. This was causing the effect to run uncontrollably using old references. To avoid this, we use the newuseEffectEventapproach...
That fix does not appear to have been applied to the plain select-field Condition/Select component, which still uses a plain useEffect for the equivalent self-correction logic.
To Reproduce
We were not able to reliably reproduce this interactively (single production occurrence, not seen again), but the mechanism suggests:
- In the list view, add a filter on any
select-type field. - Choose a multi-value operator (
is in/is not in) and select 2+ options, sovaluebecomes an array. - Switch the operator to a single-value operator (
equals, etc.) through a path that does not go throughCondition's ownhandleOperatorChangeguard — e.g. browser back/forward navigation restoring a stalewherequery string, or manually editing the URL — so thatoperatorandvaluearrive at theSelectcomponent already mismatched on mount/update, rather than being normalized byhandleOperatorChangefirst. - Observe the infinite "Maximum update depth exceeded" loop.
(Normal UI interaction — clicking the operator dropdown — does not trigger this, because Condition's handleOperatorChange already resets the value when the type doesn't match the new operator. The bug requires the mismatched state to arrive without going through that handler.)
Expected behavior
Switching a select filter between single- and multi-value operators, however the mismatched state arises, should not crash the list view.
Environment
- Payload: 3.85.1
- Adapter: @payloadcms/db-postgres 3.85.1
- Next.js: 16.2.9
- React: 19.1.0
- Node: 20.20.2
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsx and trace the self-correcting effect, its onChange callback, and the value/operator inputs. Compare the equivalent logic in WhereBuilder/Condition/Relationship and the approach described in #11080. Done means a mismatched select value and operator no longer causes a repeated update or crashes the list view.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nextjs, react, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100