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.
2. Render the home page¶
Currently, the home page route looks like this 👇
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.
Setup¶
First I'll set up a client component called Home.
Then I'll import it into the HomePage server component.
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.
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.
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.
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.
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.
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
.
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..
-
Avoid hard page loads and don't give the user a reason to refresh the page. In other words, use
router.push()
in lieu ofwindow.location.replace()
andwindow.location.assign()
. -
Display a "Loading..." UI until the user's existence has been evaluated. Here's what that might look like
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.