Skip to content

fix: return toPrimitive function instead of throwing for React 19 compat#635

Open
deggertsen wants to merge 1 commit intoLegendApp:mainfrom
deggertsen:fix/react19-symbol-to-primitive
Open

fix: return toPrimitive function instead of throwing for React 19 compat#635
deggertsen wants to merge 1 commit intoLegendApp:mainfrom
deggertsen:fix/react19-symbol-to-primitive

Conversation

@deggertsen
Copy link

Problem

React 19's development mode introduces a new logComponentRender function that inspects component props by iterating their properties and calling Symbol.toPrimitive to convert them to loggable values. When Legend State observables are passed as props (which is a common pattern), this triggers the Proxy's get trap for Symbol.toPrimitive, which currently throws:

Uncaught Error: [legend-state] observable should not be used as a primitive.
You may have forgotten to use .get() or .peek() to get the value of the observable.
    at Object.get (index.mjs:1548:13)
    at addValueToProperties (react-dom-client.development.js:3938:63)
    at addObjectDiffToProperties (react-dom-client.development.js:4050:15)
    at logComponentRender (react-dom-client.development.js:4131:22)
    at commitPassiveMountOnFiber (react-dom-client.development.js:15470:13)

This crashes the entire component tree in React 19 dev mode via the error boundary.

Solution

Change the Symbol.toPrimitive handler in the Proxy from throwing to returning a proper toPrimitive function:

  • Returns NaN for numeric hint (arithmetic operations still produce obviously wrong results)
  • Returns '[Observable]' for string/default hint (string concatenation shows the observable wasn't unwrapped)
  • Emits console.warn in development mode to help developers catch accidental primitive coercion

This preserves the developer experience (you still get a warning when you accidentally use an observable as a primitive) while being compatible with React 19's dev-mode inspection.

Why not keep the throw?

The throw was a dev-time guard to catch observable + 1 mistakes. But:

  1. It breaks React 19 — React's internals legitimately access Symbol.toPrimitive for dev logging
  2. A console.warn serves the same purpose — developers still see the warning
  3. NaN/'[Observable]' return values make the bug obvious even without the warning (you'll see NaN or [Observable] in your UI)
  4. Other Proxy-based state libraries (e.g., MobX, Valtio) handle this gracefully rather than throwing

Test Changes

Updated the existing "Adding observables should throw" test to verify the new behavior: warns + returns NaN instead of throwing.

Made with Cursor

React 19's development mode introduces `logComponentRender` which inspects
component props by calling `Symbol.toPrimitive` on them. When Legend State
observables are passed as props, this triggers the proxy's `get` trap for
`Symbol.toPrimitive`, which previously threw an error. This crashes the
entire component tree in React 19 dev mode.

The fix changes the behavior from throwing to:
- Returning a proper `Symbol.toPrimitive` function (`NaN` for numeric
  hint, `'[Observable]'` for string/default hint)
- Emitting a `console.warn` in development mode to still help developers
  catch accidental primitive coercion

This is a non-breaking change for production code (the throw was only
useful as a dev-time guard), and it's consistent with how other
Proxy-based state libraries handle this scenario.

Fixes React 19 compatibility where `addValueToProperties` →
`logComponentRender` in react-dom-client would crash when encountering
Legend State observables.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

1 participant