diff --git a/docs/reference/functions/AuthCheck.md b/docs/reference/functions/AuthCheck.md index 9bedbb15..91702f7b 100644 --- a/docs/reference/functions/AuthCheck.md +++ b/docs/reference/functions/AuthCheck.md @@ -8,7 +8,7 @@ > **AuthCheck**(`__namedParameters`): `Element` -Defined in: [src/auth.tsx:247](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L247) +Defined in: [src/auth.tsx:257](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L257) ## Parameters diff --git a/docs/reference/functions/ClaimsCheck.md b/docs/reference/functions/ClaimsCheck.md index 8fa527d8..5138d974 100644 --- a/docs/reference/functions/ClaimsCheck.md +++ b/docs/reference/functions/ClaimsCheck.md @@ -8,7 +8,7 @@ > **ClaimsCheck**(`__namedParameters`): `Element` -Defined in: [src/auth.tsx:203](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L203) +Defined in: [src/auth.tsx:213](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L213) ## Parameters diff --git a/docs/reference/functions/useIdTokenResult.md b/docs/reference/functions/useIdTokenResult.md index caa6c9d5..a8e573dd 100644 --- a/docs/reference/functions/useIdTokenResult.md +++ b/docs/reference/functions/useIdTokenResult.md @@ -8,7 +8,7 @@ > **useIdTokenResult**(`user`, `forceRefresh?`, `options?`): [`ObservableStatus`](../type-aliases/ObservableStatus.md)\<`IdTokenResult`\> -Defined in: [src/auth.tsx:31](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L31) +Defined in: [src/auth.tsx:41](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L41) ## Parameters diff --git a/docs/reference/functions/useSigninCheck.md b/docs/reference/functions/useSigninCheck.md index d8a7c15d..928d22e5 100644 --- a/docs/reference/functions/useSigninCheck.md +++ b/docs/reference/functions/useSigninCheck.md @@ -8,7 +8,7 @@ > **useSigninCheck**(`options?`): [`ObservableStatus`](../type-aliases/ObservableStatus.md)\<[`SigninCheckResult`](../type-aliases/SigninCheckResult.md)\> -Defined in: [src/auth.tsx:124](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L124) +Defined in: [src/auth.tsx:134](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L134) Subscribe to the signed-in status of a user. diff --git a/docs/reference/interfaces/AuthCheckProps.md b/docs/reference/interfaces/AuthCheckProps.md index d24c0e50..37e7b895 100644 --- a/docs/reference/interfaces/AuthCheckProps.md +++ b/docs/reference/interfaces/AuthCheckProps.md @@ -6,7 +6,7 @@ # Interface: AuthCheckProps -Defined in: [src/auth.tsx:42](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L42) +Defined in: [src/auth.tsx:52](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L52) ## Properties @@ -14,7 +14,7 @@ Defined in: [src/auth.tsx:42](https://github.com/FirebaseExtended/reactfire/blob > **children**: `ReactNode` -Defined in: [src/auth.tsx:44](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L44) +Defined in: [src/auth.tsx:54](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L54) *** @@ -22,7 +22,7 @@ Defined in: [src/auth.tsx:44](https://github.com/FirebaseExtended/reactfire/blob > **fallback**: `ReactNode` -Defined in: [src/auth.tsx:43](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L43) +Defined in: [src/auth.tsx:53](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L53) *** @@ -30,4 +30,4 @@ Defined in: [src/auth.tsx:43](https://github.com/FirebaseExtended/reactfire/blob > `optional` **requiredClaims?**: `Object` -Defined in: [src/auth.tsx:45](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L45) +Defined in: [src/auth.tsx:55](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L55) diff --git a/docs/reference/interfaces/ClaimCheckErrors.md b/docs/reference/interfaces/ClaimCheckErrors.md index 6e4bc87d..470ca5ab 100644 --- a/docs/reference/interfaces/ClaimCheckErrors.md +++ b/docs/reference/interfaces/ClaimCheckErrors.md @@ -6,7 +6,7 @@ # Interface: ClaimCheckErrors -Defined in: [src/auth.tsx:55](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L55) +Defined in: [src/auth.tsx:65](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L65) ## Indexable diff --git a/docs/reference/interfaces/ClaimsCheckProps.md b/docs/reference/interfaces/ClaimsCheckProps.md index 9ea853a1..4d39e79b 100644 --- a/docs/reference/interfaces/ClaimsCheckProps.md +++ b/docs/reference/interfaces/ClaimsCheckProps.md @@ -6,7 +6,7 @@ # Interface: ClaimsCheckProps -Defined in: [src/auth.tsx:48](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L48) +Defined in: [src/auth.tsx:58](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L58) ## Properties @@ -14,7 +14,7 @@ Defined in: [src/auth.tsx:48](https://github.com/FirebaseExtended/reactfire/blob > **children**: `ReactNode` -Defined in: [src/auth.tsx:51](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L51) +Defined in: [src/auth.tsx:61](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L61) *** @@ -22,7 +22,7 @@ Defined in: [src/auth.tsx:51](https://github.com/FirebaseExtended/reactfire/blob > **fallback**: `ReactNode` -Defined in: [src/auth.tsx:50](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L50) +Defined in: [src/auth.tsx:60](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L60) *** @@ -30,7 +30,7 @@ Defined in: [src/auth.tsx:50](https://github.com/FirebaseExtended/reactfire/blob > **requiredClaims**: `object` -Defined in: [src/auth.tsx:52](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L52) +Defined in: [src/auth.tsx:62](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L62) #### Index Signature @@ -42,4 +42,4 @@ Defined in: [src/auth.tsx:52](https://github.com/FirebaseExtended/reactfire/blob > **user**: `User` -Defined in: [src/auth.tsx:49](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L49) +Defined in: [src/auth.tsx:59](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L59) diff --git a/docs/reference/interfaces/ClaimsValidator.md b/docs/reference/interfaces/ClaimsValidator.md index e52f115e..feb558fd 100644 --- a/docs/reference/interfaces/ClaimsValidator.md +++ b/docs/reference/interfaces/ClaimsValidator.md @@ -6,11 +6,11 @@ # Interface: ClaimsValidator() -Defined in: [src/auth.tsx:81](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L81) +Defined in: [src/auth.tsx:91](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L91) > **ClaimsValidator**(`claims`): `object` -Defined in: [src/auth.tsx:82](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L82) +Defined in: [src/auth.tsx:92](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L92) ## Parameters diff --git a/docs/reference/interfaces/SignInCheckOptionsBasic.md b/docs/reference/interfaces/SignInCheckOptionsBasic.md index b581c6c6..6c9f2d66 100644 --- a/docs/reference/interfaces/SignInCheckOptionsBasic.md +++ b/docs/reference/interfaces/SignInCheckOptionsBasic.md @@ -6,7 +6,7 @@ # Interface: SignInCheckOptionsBasic -Defined in: [src/auth.tsx:73](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L73) +Defined in: [src/auth.tsx:83](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L83) ## Extends @@ -23,7 +23,7 @@ Defined in: [src/auth.tsx:73](https://github.com/FirebaseExtended/reactfire/blob > `optional` **forceRefresh?**: `boolean` -Defined in: [src/auth.tsx:74](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L74) +Defined in: [src/auth.tsx:84](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L84) *** diff --git a/docs/reference/interfaces/SignInCheckOptionsClaimsObject.md b/docs/reference/interfaces/SignInCheckOptionsClaimsObject.md index 43a884ee..092cde82 100644 --- a/docs/reference/interfaces/SignInCheckOptionsClaimsObject.md +++ b/docs/reference/interfaces/SignInCheckOptionsClaimsObject.md @@ -6,7 +6,7 @@ # Interface: SignInCheckOptionsClaimsObject -Defined in: [src/auth.tsx:77](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L77) +Defined in: [src/auth.tsx:87](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L87) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/auth.tsx:77](https://github.com/FirebaseExtended/reactfire/blob > `optional` **forceRefresh?**: `boolean` -Defined in: [src/auth.tsx:74](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L74) +Defined in: [src/auth.tsx:84](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L84) #### Inherited from @@ -54,7 +54,7 @@ Defined in: [src/index.ts:26](https://github.com/FirebaseExtended/reactfire/blob > **requiredClaims**: `ParsedToken` -Defined in: [src/auth.tsx:78](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L78) +Defined in: [src/auth.tsx:88](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L88) *** diff --git a/docs/reference/interfaces/SignInCheckOptionsClaimsValidator.md b/docs/reference/interfaces/SignInCheckOptionsClaimsValidator.md index aeec944b..83517d7a 100644 --- a/docs/reference/interfaces/SignInCheckOptionsClaimsValidator.md +++ b/docs/reference/interfaces/SignInCheckOptionsClaimsValidator.md @@ -6,7 +6,7 @@ # Interface: SignInCheckOptionsClaimsValidator -Defined in: [src/auth.tsx:88](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L88) +Defined in: [src/auth.tsx:98](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L98) ## Extends @@ -18,7 +18,7 @@ Defined in: [src/auth.tsx:88](https://github.com/FirebaseExtended/reactfire/blob > `optional` **forceRefresh?**: `boolean` -Defined in: [src/auth.tsx:74](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L74) +Defined in: [src/auth.tsx:84](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L84) #### Inherited from @@ -82,4 +82,4 @@ Defined in: [src/index.ts:31](https://github.com/FirebaseExtended/reactfire/blob > **validateCustomClaims**: [`ClaimsValidator`](ClaimsValidator.md) -Defined in: [src/auth.tsx:89](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L89) +Defined in: [src/auth.tsx:99](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L99) diff --git a/docs/reference/type-aliases/SigninCheckResult.md b/docs/reference/type-aliases/SigninCheckResult.md index 29f5bec9..bb80be74 100644 --- a/docs/reference/type-aliases/SigninCheckResult.md +++ b/docs/reference/type-aliases/SigninCheckResult.md @@ -8,4 +8,4 @@ > **SigninCheckResult** = \{ `errors`: \{ \}; `hasRequiredClaims`: `false`; `signedIn`: `false`; `user`: `null`; \} \| \{ `errors`: [`ClaimCheckErrors`](../interfaces/ClaimCheckErrors.md); `hasRequiredClaims`: `boolean`; `signedIn`: `true`; `user`: `User`; \} -Defined in: [src/auth.tsx:59](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L59) +Defined in: [src/auth.tsx:69](https://github.com/FirebaseExtended/reactfire/blob/main/src/auth.tsx#L69) diff --git a/src/auth.tsx b/src/auth.tsx index d535b894..7a1c4dfe 100644 --- a/src/auth.tsx +++ b/src/auth.tsx @@ -25,7 +25,17 @@ export function useUser(options?: ReactFireOptions): ObservableS const observableId = `auth:user:${auth.name}`; const observable$ = user(auth); - return useObservable(observableId, observable$, options); + const _options: ReactFireOptions = { ...options }; + + // If a user is already signed in, seed initialData so consumers see the user + // 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)) { + _options.initialData = auth.currentUser as unknown as T; + } + + return useObservable(observableId, observable$, _options); } export function useIdTokenResult(user: User, forceRefresh = false, options?: ReactFireOptions): ObservableStatus { diff --git a/src/useObservable.ts b/src/useObservable.ts index 08e8c748..fe05be5d 100644 --- a/src/useObservable.ts +++ b/src/useObservable.ts @@ -125,11 +125,24 @@ export function useObservable(observableId: string, source: Observa const update = useSyncExternalStore(subscribe, getSnapshot); - // modify the value if initialData exists + // Return a new object with initialData overlaid rather than mutating the shared + // _immutableStatus reference, which is the same object across all components + // using the same observableId. if (!observable.hasValue && hasData) { - update.data = config?.initialData ?? config?.startWithValue; - update.status = 'success'; - update.hasEmitted = true; + const initialDataValue = config?.initialData ?? config?.startWithValue; + + // In suspense mode, throw errors so React Error Boundaries can catch them. + // In non-suspense mode, surface errors via status so consumers can handle them locally. + if (suspenseEnabled && update.error) { + throw update.error; + } + + return { + ...update, + data: initialDataValue, + status: 'success', + hasEmitted: true, + } as ObservableStatus; } // throw an error if there is an error diff --git a/test/auth.test.tsx b/test/auth.test.tsx index 61656426..c717754a 100644 --- a/test/auth.test.tsx +++ b/test/auth.test.tsx @@ -1,6 +1,8 @@ import { cleanup, render, waitFor, renderHook, act } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import * as React from 'react'; +import { NEVER } from 'rxjs'; +import { preloadObservable } from '../src/useObservable'; import { FirebaseAppProvider, AuthCheck, @@ -326,6 +328,39 @@ describe('Authentication', () => { expect(result.current.data).toEqual(getAuth(app).currentUser); }); + it('synchronously returns the current user without waiting for the observable', async () => { + await act(async () => { + await signIn(); + }); + + // Replace the auth:user observable with NEVER so it never emits. + // Without the fix (no initialData seeding), status is 'loading' on first render. + // With the fix (initialData = auth.currentUser), status is 'success' synchronously. + const cache = (globalThis as any)._reactFirePreloadedObservables as Map; + const authUserKey = `auth:user:${getAuth(app).name}`; + cache?.delete(authUserKey); + preloadObservable(NEVER, authUserKey); + + let capturedFirstRender: { user: any; status: string } | undefined; + + const UserComponent = () => { + const { data: user, status } = useUser(); + if (capturedFirstRender === undefined) { + capturedFirstRender = { user, status }; + } + return {String(status)}; + }; + + try { + render(, { wrapper: Provider }); + + expect(capturedFirstRender!.status).toBe('success'); + expect(capturedFirstRender!.user).toEqual(getAuth(app).currentUser); + } finally { + cache?.delete(authUserKey); + } + }); + it('does not show a logged-out user after navigating away', async () => { await signIn();