fix: seed useUser with auth.currentUser to prevent undefined on initial render#731
fix: seed useUser with auth.currentUser to prevent undefined on initial render#731tyler-reitz wants to merge 4 commits into
Conversation
Restores synchronous user access that was removed in 4.2.3 (FirebaseExtended#539). Without it, child components calling useUser() see status:'loading' / data:undefined on first render even when the user is already signed in, because a fresh SuspenseSubject has to wait for the async onAuthStateChanged callback before emitting. Only seeds initialData when auth.currentUser is truthy, so the uninitialized-auth case (currentUser is null before SDK reads from storage) is not incorrectly treated as signed-out. Fixes FirebaseExtended#582
There was a problem hiding this comment.
Code Review
This pull request updates the useUser hook in src/auth.tsx to seed initialData with auth.currentUser when a user is already signed in, enabling synchronous rendering on the first load. The review feedback points out that this implementation unconditionally overwrites any caller-provided initialData or startWithValue options, and suggests only seeding the user if these options are not already explicitly defined.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Two asks, non-blocking since I'm approving:
- Add a regression test that fails on main. The test you cite passes on main in test suite runs, so nothing today stops a future #539-style removal from silently regressing #582 again. The guard-pattern structure works even mid-suite because
useSigninCheckanduseUsercache under different ids: parent renders auseSigninCheckguard; oncesignedIn, mount a child callinguseUser(); assert the child's first-renderdatais the user. Happy to share the exact test I used. - Correct the Testing section of the PR body (it becomes the squash-commit message): "All 14 auth tests pass" is equally true on main, and the cited test only discriminates when run in isolation. With ask 1 done, the body can simply point at the new test.
A couple more non-blocking notes that didn't fit as inline comments:
- Is it Intentional that
useSigninCheckstays unseeded? Post-merge, the two auth hooks disagree on first-render state for a signed-in user (useUser→ instantsuccess,useSigninCheck→loadingflash). jhuleatt's comment in #582 pointed at the two hooks using different observables as the root problem; warming/sharing fromuseSigninCheck(no-claims path) would close the asymmetry and has nocurrentUser-timing windows at all. Fine as a follow-up — asking mainly whether it's on your radar. - Since #539's objection was "undocumented tri-state," worth one line in the body noting the ground has shifted: the SDK now documents
currentUser: User | nulland exposesauthStateReady(). Truthiness is still a fine choice here (authStateReady()is async and can't help a synchronous first render), but pre-empting the #539 argument in the body will save a review cycle if Jeff looks at this. - A component that mounts while
signOut()is in flight will now show the outgoing user on its first frame and see noloadingstatus for that mount (success(user)→success(null)). Main already showed a stale signed-in frame one paint later in the same window, so this is a one-frame delta, not a new failure mode — but if someone ever reports "flash of signed-in UI during logout," this is where it lives.
Approving.
| // synchronously on the first render without waiting for the async observable. | ||
| // We only do this when currentUser is truthy to avoid masking the uninitialized | ||
| // (null before auth has loaded from storage) case as "signed out". | ||
| if (auth.currentUser && !('initialData' in _options) && !('startWithValue' in _options)) { |
There was a problem hiding this comment.
Consider documenting the behavior change where users will see it: one JSDoc sentence on useUser (first render is success for already-signed-in users; in suspense mode, signed-in users no longer suspend) + a docs regen. The suspense change is arguably the biggest surface change and it's currently only visible by reading this code comment.
| // We only do this when currentUser is truthy to avoid masking the uninitialized | ||
| // (null before auth has loaded from storage) case as "signed out". | ||
| if (auth.currentUser && !('initialData' in _options) && !('startWithValue' in _options)) { | ||
| _options.initialData = auth.currentUser as unknown as T; |
There was a problem hiding this comment.
initialData's type collapses to any, so this assigns with no cast at all (verified with the repo's own tsc). If you keep a cast, the codebase's existing idiom is as any as.
Problem
Since 4.2.3 (#539 removed the
initialDataseeding),useUser()returns{status: 'loading', data: undefined}on the first render when called in a child component whose parent only calleduseSigninCheck()as a guard.The root cause:
useSigninCheckanduseUseruse different observable IDs (auth:signInCheck:...vsauth:user:...). When the guard resolves and renders the child, the child'suseUser()creates a freshSuspenseSubjectthat hasn't received a value yet. Firebase'sonAuthStateChangedfires asynchronously, so the first snapshot is alwaysloading.This breaks patterns like:
Fix
Restore the synchronous seed: if
auth.currentUseris truthy (a signed-in user is available), set it asinitialDatabefore passing touseObservable. This meansuseUser()returns the user immediately on the first render without waiting for the async observable.The check is
if (auth.currentUser)(truthy, not!== undefined) to avoid the ambiguous case wherecurrentUserisnullbefore auth has finished loading from storage. If we seedednullin that case, callers would incorrectly see a signed-out state during initialization.Testing
synchronously returns a user if one is already signed incovers the exact behavior restored hereFixes #582