Skip to content

WhereBuilder/Condition/Select: "Maximum update depth exceeded" when filter value/operator multiplicity mismatch (sibling to already-fixed #11080 Relationship filter bug) #17248

Description

@jelitz

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:

  1. In the list view, add a filter on any select-type field.
  2. Choose a multi-value operator (is in / is not in) and select 2+ options, so value becomes an array.
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions