Ben Gorman

Ben Gorman

Life's a garden. Dig it.

Recap

So far, we've implemented a basic form where users can sign up with their email and password. A successful response from the server returns an id token (AKA access token) as a JWT. Furthermore, Firebase does some behind-the-scenes work to place the user data into local storage.

Next Step

After a user successfully signs in, let's redirect them to the home page /.

Furthermore, let's design the home page so that

  • Signed in users sees a "sign out" button
  • Anonymous users see a "sign in" button and a "sign up" button

1. Redirect after sign up

For this, I'll use useRouter from Next.js. Upon successful sign up, router.push("/") redirects the user to the home page.

src/app/SignUpForm.js
"use client"
 
import { auth } from "@/firebase/firebase"
import { createUserWithEmailAndPassword } from "firebase/auth"
import { useRouter } from "next/navigation"
import { useState } from "react"
 
export function SignUpForm() {
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")
  const router = useRouter()
 
  const handleFormSubmit = (event) => {
    event.preventDefault()
 
    createUserWithEmailAndPassword(auth, email, password)
      .then((userCredential) => {
        router.push("/")
      })
      .catch((error) => {
        console.log("Error:", error)
      })
  }
 
  // No changes from this point onward
  // ...
}

2. Render the home page

Currently, the home page route looks like this 👇

src/app/page.js
export default function HomePage() {
  return <div>Home page</div>
}

This is a server component. It is rendered on the server and the resulting HTML is delivered to the client.

If we're going to make this component dynamic based on the user's authenticated state, then we need to answer a fundamental question...

How does this server component know anything about the user who requested the page?

To make this work, we'd need to send information about the user from the client to the server. Presumably, we could send the user's idToken to the server.

However, this is not the default behavior for Firebase web apps. Rather, Firebase expects us to dynamically render components client-side. So, let's do that.

I'll show you how to do things server-side later. For now, let's do things the Firebase way, client-side.

Setup

First I'll set up a client component called Home.

src/app/Home.jsx
"use client"
 
export function Home() {
  return <div>To do..</div>
}

Then I'll import it into the HomePage server component.

src/app/page.js
import { Home } from "./Home"
 
export default function HomePage() {
  return (
    <>
      <div>Home page</div>
      <Home />
    </>
  )
}

Now comes the logic to make Home.jsx show dynamic content depending on the authenticated state of the user.

Attempt 1 (auth.currentUser)

Firebase Auth provides a currentUser property, so lets try using that.

src/app/Home.jsx
"use client"
 
import { auth } from "@/firebase/firebase"
import Link from "next/link"
 
export function Home() {
  return (
    auth.currentUser
      ? <button>Sign out</button>
      : <>
        <Link>Sign up</Link>
        <Link>Sign in</Link>
      </>
  )
}

At first glance, this appears to work, but there's actually a pretty big flaw in this design.

Refreshing the home page causes the currently authenticated user to see the unauthenticated view.

Why?

Don't quote me on this, but here's my understanding..

auth is an Auth instance. It's instantiated inside the file src/firebase/firebase.js. Note that this code is not inside of a React component.

When the user signs up, the currentUser property of auth gets populated.

router.push("/") then redirects the user to the home page. This is a special, Next.js navigation that does client-side caching. In particular, the auth instance is cached. I.e. auth is NOT reinstantiated after router.push("/").

Tip

Add a console.log() statement where auth is instantiated to see when it happens.

src/firebase/firebase.js
...
 
// Instantiate services
console.log("Instantiating auth")
export const auth = getAuth(app)
 
...

If user does a normal refresh, the auth instance is deleted and then re-instantiated.

That reinstantiation process involves making a POST request to fetch the user details from the Firebase Authentication backend. (In React terms, this is known as a side-effect.)

At the time React renders the Home component (after the user manually refreshes the page), auth.currentUser is undefined. Hence, the unauthenticated version of the page is rendered.

Attempt 2 (IndexedDB)

Earlier we noted that, on signup, Firebase stores information about the user in IndexedDB.

firebaseLocalStorageDb

In theory, we could use this information to know details about the user, and adjust the homepage accordingly. The problem is that this data and strategy is undocumented. Firebase may change or abandon this architecture at any time, without warning.

If you're still interested in this idea, here's some code to help you get started.

Read user data from firebaseLocalStorageDb
const request = indexedDB.open("firebaseLocalStorageDb")
 
request.onerror = (event) => {
  console.error("Something's wrong!")
}
 
request.onsuccess = (event) => {
  var db = event.target.result
  var trans = db.transaction("firebaseLocalStorage", "readonly")
  var store = trans.objectStore("firebaseLocalStorage")
 
  store.getAll().onsuccess = (event) => {
    console.log(`User: ${ JSON.stringify(event.target.result) }`)
  }
}

Attempt 3 (onAuthStateChanged)

Now let's do things the way Firebase intended us to. The key to this method is to use the onAuthStateChanged observer.

src/app/Home.jsx
"use client"
 
import { auth } from "@/firebase/firebase"
import { onAuthStateChanged } from "firebase/auth"
import Link from "next/link"
import { useEffect, useState } from "react"
 
export function Home() {
  const [authUser, setAuthUser] = useState()
 
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      if (user) {
        // User is signed in
        console.log("usr", JSON.stringify(user))
        setAuthUser(user)
      } else {
        // User is signed out
      }
    })
 
    return unsubscribe
  }, [])
 
 
  return (
    authUser
      ? <button>Sign out</button>
      : <>
        <Link href="/signup">Sign up</Link>
        <span> | </span>
        <Link href="/signin">Sign in</Link>
      </>
  )
}

Let's this thing in action!

Explanation
The onAuthStateChanged() observer fires whenever there's a change to the authenticated state of the current user. When router.push("/") redirects the user to the home page, the user is already signed in, so we see the Sign Out button. But when we refresh the page, the auth object re-instantiates, and for a moment, authUser is undefined.

There's also a bit of React madness going on here.. onAuthStateChanged() returns an unsubscribe function. By returning that function inside useEffect(), it ensures that the listener will be cleaned up when the component unmounts.

If Firebase has to fetch the user details from an API, how does it know which user to look up?
If you monitor the Network tab after forcing the page to refresh, you'll notice this POST request made to the googleapis.com/v1/accounts:looup.

Account lookup

The payload of the request includes the user's idToken. Guess where this idToken comes from.. the same firebaseLocalStorage IndexedDB we discussed earlier!

How do I prevent the flicker when the page reloads?
As discussed, the flicker is caused by that brief moment when the auth object is uninstantiated. You have a few options to deal with this..

  1. Avoid hard page loads and don't give the user a reason to refresh the page. In other words, use router.push() in lieu of window.location.replace() and window.location.assign().

  2. Display a "Loading..." UI until the user's existence has been evaluated. Here's what that might look like

    src/app/Home.jsx
    "use client"
     
    import { auth } from "@/firebase/firebase"
    import { onAuthStateChanged } from "firebase/auth"
    import Link from "next/link"
    import { useEffect, useState } from "react"
     
    export function Home() {
      // initial state of authUser is undefined
      const [authUser, setAuthUser] = useState(undefined)
     
      useEffect(() => {
        const unsubscribe = onAuthStateChanged(auth, (user) => {
          // user will be an object or null
          setAuthUser(user)
        })
        return unsubscribe
      }, [])
     
      return (
        authUser === undefined
          ? <div>Loading...</div>
          : (
            authUser 
              ? <button>Sign out</button>
              : <>
                <Link href="/signup">Sign up</Link>
                <span> | </span>
                <Link href="/signin">Sign in</Link>
              </>
          )
      )
    }

These "solutions" make for a better user experience, but they still don't really solve the problem. It should be clear that what we really want is for the server to work out the initial HTML that should be rendered for the user.

Soon, I'll show you how to offload this logic onto the server, but first let's implement the Sign in and Sign out components.