You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
getSnapshot() returns observable.immutableStatus, which is SuspenseSubject._immutableStatus — a single shared object reference for all components using the same observableId. Mutating it in-place means one component's render corrupts the snapshot for every other component sharing that observable.
Concrete failure scenario
Two components both call useUser() for the same auth instance (same observableId → same SuspenseSubject → same _immutableStatus object):
Component B calls getSnapshot() — it gets the same already-mutated object and renders status: 'success' with a user, even though it passed no initialData and the observable hasn't emitted yet.
useSyncExternalStore's tearing detection does not catch this: Object.is() on the same reference always returns true, so React treats the store as unchanged.
_updateImmutableStatus() (called on each observable emission) creates a new object — so the mutation is eventually overwritten. But between mount and first emission, the shared snapshot is wrong.
Why this surfaced now
This was a pre-existing bug, but it was only triggered when callers explicitly passed initialData. PR #731 seeds initialData from auth.currentUser on every mount of a signed-in user, making this path hit far more frequently.
Bug
useObservableappliesinitialDataby mutating the object returned bygetSnapshot()in-place (lines 130-132 ofsrc/useObservable.ts):getSnapshot()returnsobservable.immutableStatus, which isSuspenseSubject._immutableStatus— a single shared object reference for all components using the sameobservableId. Mutating it in-place means one component's render corrupts the snapshot for every other component sharing that observable.Concrete failure scenario
Two components both call
useUser()for the same auth instance (sameobservableId→ sameSuspenseSubject→ same_immutableStatusobject):initialDataset (e.g.,auth.currentUseris truthy after fix: seed useUser with auth.currentUser to prevent undefined on initial render #731). It mutates_immutableStatus.status = 'success'and.data = user.getSnapshot()— it gets the same already-mutated object and rendersstatus: 'success'with a user, even though it passed noinitialDataand the observable hasn't emitted yet.useSyncExternalStore's tearing detection does not catch this:Object.is()on the same reference always returnstrue, so React treats the store as unchanged._updateImmutableStatus()(called on each observable emission) creates a new object — so the mutation is eventually overwritten. But between mount and first emission, the shared snapshot is wrong.Why this surfaced now
This was a pre-existing bug, but it was only triggered when callers explicitly passed
initialData. PR #731 seedsinitialDatafromauth.currentUseron every mount of a signed-in user, making this path hit far more frequently.Fix
Replace the in-place mutation with a new object:
This keeps the
SuspenseSubject._immutableStatusobject untouched and gives each render its own snapshot.Affected versions
All versions that support
initialData/startWithValue. Worsened in 4.2.4+ after #731.