FirebaseExtended / reactfire

Hooks, Context Providers, and Components that make it easy to interact with Firebase.
https://firebaseopensource.com/projects/firebaseextended/reactfire/
MIT License
3.54k stars 401 forks source link

useUser() returns undefined unexpectedly on initial render #582

Open wieringen opened 1 year ago

wieringen commented 1 year ago

Version info

React: 18.2.0 Next: 13.2.4 Firebase: 9.22.1 ReactFire: 4.2.3

Steps to reproduce

I initialize reactfire using the code below. As you can see, I have a guard checking if the user is signed in. And only if the user is signed in the children/pages are rendered.

function AuthWrapper(props: any) {
  const app = useFirebaseApp();
  const auth = getAuth(app);
  return <AuthProvider sdk={auth}>{props.children}</AuthProvider>;
}

function AuthGuard(props: React.PropsWithChildren<{ fallback: JSX.Element }>) {
  const router = useRouter();
  const { status, data: signInCheckResult } = useSigninCheck();
  const { data: user } = useUser();

  if (status === "loading") {
    return props.fallback;
  }
  if (signInCheckResult?.signedIn === true) {
    return props.children as JSX.Element;
  } else {
    return router.push("/login");
  }
}

export default function App({
  Component,
  pageProps
}: MyAppProps) {
  return (
    <FirebaseAppProvider firebaseConfig={firebaseConfig}>
      <AuthWrapper>
        <AuthGuard fallback={<PageLoader />}>
          <TeamProvider>
            <Component {...pageProps} />
          </TeamProvider>
        </AuthGuard>
      </AuthWrapper>
    </FirebaseAppProvider>
  );
}

In my page component, I try to retrieve the user, but on the initial render the user is undefined. This results in useIdtokenResult throwing an error (Error: you must provide a user) since no user is given and that is required. I need useIdTokenResult because the component needs those values at a later stage.

export default function EditRouteForm() {
  const { data: user } = useUser();
  const { data: idTokenResult } = useIdTokenResult(user as User, false, {
    initialData: { claims: { group: "", roles: [] } },
  });
  const { group, roles } = idTokenResult.claims
 /* ...... */
}

Expected behavior

Because of my auth guard in the AuthCheck component, I'm expecting useUser to always return an user.

Actual behavior

The initial render of useUser always returns undefined.

jhuleatt commented 1 year ago

Hi @wieringen, does this happen with ReactFire 4.2.2? 4.2.3 released a change to useUser, and I'm wondering if we may have broken something.

We have a test to guard against this exact issue, so I'm a little surprised it is happening at all: https://github.com/FirebaseExtended/reactfire/blob/363c7c65d2853aa16bbad8b8f13ad81efadf9cea/test/auth.test.tsx#L320-L327

jhuleatt commented 1 year ago

Actually, I wonder if this could be related to mixing userUser and useSigninCheck. They should both agree, but I don't think we test for it, so that could be the issue. Could you please try logging the results of both in your AuthGuard component to see if they ever disagree?

Something like:

const { status, data: signInCheckResult } = useSigninCheck();
const { data: user } = useUser();

if (status === "loading") {
  return props.fallback;
}

console.assert(user === signInCheckResult.user, user, signInCheckResult.user);
wieringen commented 1 year ago

I'm using the latest version 4.2.3. I did some more testing based on your suggestions. When I change the AuthGuard to the code below it works. Notice how the user variable is unused. When I leave out the useUser hook, I get the error in my Page component again.

function AuthGuard(props: React.PropsWithChildren<{ fallback: JSX.Element }>) {
  const router = useRouter();
  const { status, data: signInCheckResult } = useSigninCheck();
  const { data: user } = useUser();

  if (status === "loading") {
    return props.fallback;
  }
  if (signInCheckResult?.signedIn === true) {
    return props.children as JSX.Element;
  } else {
    return router.push("/login");
  }
}
jthetzel commented 1 year ago

We had a similar issue with useUser() in 4.2.3 returning on initial load:

{ 
  data: undefined, 
  status: 'loading',
  ...
}

Pinning to 4.2.2 resolved the error, with useUser() returning on initial load:

{ 
  data: {
    emailVerified: true,
    ...
  }, 
  status: 'success',
  ...
}
das-monki commented 7 months ago

Had the same issue, user was undefined on initial render. What solved it for me was adding suspense=true at <FirebaseAppProvider suspense=true />.

See also following comment at the (deprecated) AuthCheck:

Meant for Concurrent mode only (`<FirebaseAppProvider suspense=true />`).