diff --git a/src/ObservableObject.ts b/src/ObservableObject.ts index 24911aed..51d75c17 100644 --- a/src/ObservableObject.ts +++ b/src/ObservableObject.ts @@ -395,11 +395,16 @@ export function flushPending() { const proxyHandler: ProxyHandler = { get(node: NodeInfo, p: any, receiver: any) { if (p === symbolToPrimitive) { - throw new Error( - process.env.NODE_ENV === 'development' - ? '[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.' - : '[legend-state] observable is not a primitive.', - ); + // Return a toPrimitive function instead of throwing so that external code + // (e.g. React 19's dev-mode logComponentRender) can safely coerce observables + // without crashing. A dev-mode warning is still emitted to help catch + // accidental primitive usage in user code. + if (process.env.NODE_ENV === 'development') { + console.warn( + '[legend-state] observable is being converted to a primitive. You may have forgotten to use .get() or .peek() to get the value of the observable.', + ); + } + return (hint: string) => (hint === 'number' ? NaN : '[Observable]'); } if (p === symbolGetNode) { return node; diff --git a/tests/tests.test.ts b/tests/tests.test.ts index f8fda2f9..c2bf023b 100644 --- a/tests/tests.test.ts +++ b/tests/tests.test.ts @@ -3493,16 +3493,26 @@ describe('_', () => { }); }); describe('Built-in functions', () => { - test('Adding observables should throw', () => { + test('Adding observables should warn and return NaN', () => { const obs = observable({ x: 0, y: 0 }); const x = obs.x; const y = obs.y; - expect(() => { - // @ts-expect-error Testing error - x + y; - }).toThrowError(/observable is not a primitive/); + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + + // @ts-expect-error Testing primitive coercion + const result = x + y; + + // Should produce NaN since both observables coerce to NaN for numeric hint + expect(result).toBeNaN(); + + // Should warn in development mode + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('observable is being converted to a primitive'), + ); + + warnSpy.mockRestore(); }); }); describe('setAtPath', () => {