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 RelationshipFilter component was previously relying on a useEffect that had a callback in its dependencies. This was causing the effect to run uncontrollably using old references. To avoid this, we use the new useEffectEvent approach...
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, so value becomes an array.
- Switch the operator to a single-value operator (
equals, etc.) through a path that does not go through Condition's own handleOperatorChange guard — e.g. browser back/forward navigation restoring a stale where query string, or manually editing the URL — so that operator and value arrive at the Select component already mismatched on mount/update, rather than being normalized by handleOperatorChange first.
- 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
Describe the Bug
packages/ui/src/elements/WhereBuilder/Condition/Select/index.tsxcontains a self-correctinguseEffectthat can enter an infinite render loop, crashing the list view with: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:If
valueis an array whileisMultiisfalse(i.e. the condition's operator no longer expects an array, but the value hasn't been normalized to a scalar yet), this effect callsonChange(value[0]). If the parent'supdateConditionhandler round-trips back avaluethat 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/Relationshipcomponent via #11080 ("fix(ui): relationship filter renders stale values when changing fields"), which explicitly called out:That fix does not appear to have been applied to the plain
select-fieldCondition/Selectcomponent, which still uses a plainuseEffectfor 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:
select-type field.is in/is not in) and select 2+ options, sovaluebecomes an array.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.(Normal UI interaction — clicking the operator dropdown — does not trigger this, because
Condition'shandleOperatorChangealready 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
selectfilter between single- and multi-value operators, however the mismatched state arises, should not crash the list view.Environment