Waiting for auth to settle outside of templates
load
FunctionsUsing Firebase Authentication involves 3 steps:
Once you have the auth service you can trigger auth actions such as signing a user in or out and you can access the auth.currentUser
property to get the current user.
Except, you can’t …
Although the firebase docs give an example of doing exactly this, the reality is that if you access the currentUser
property immediately then chances are it’s going to be null. They have a note in small-print right after the example-that-will-never-work:
Note:
currentUser
might also be null because the auth object has not finished initializing. If you use an observer to keep track of the user’s sign-in status, you don’t need to handle this case.
The proper way to get the current user is to wait for the SDK to initialize and tell you the state using the onAuthStateChanged
function.
If you’ve read any of my previous articles, you’ll know I like to wrap this code in a Svelte store so the auth state then becomes an observable that can be used throughout the app.
Great, so what’s the problem?
Because the template will respond to the observable it’s easy to handle the different states that the auth store may go through. For instance, we can show a loading spinner until we know the auth state, and then the appropriate content once we do:
{#if $user === undefined}
<Spinner />
{:else}
{#if $user}
<YourData />
{:else}
<AccessDenied />
{/if}
{/if}
This assumes that the auth store will contain undefined
until the state is known, then either a Firebase User object
or null
to indicate authenticate / non-authenticated states which can be implemented quite easily:
import { readable } from 'svelte/store'
import { auth } from './firebase'
import { onAuthStateChanged } from 'firebase/auth'
export const user = readable<User | null>(
undefined,
set => onAuthStateChanged(auth, set)
)
The key thing is that what the template renders changes as the store changes.
load
FunctionsBut suppose you have a SvelteKit load
function that needs to load some data from Firestore based on the users identity or restrict access to unauthorized users. An easy mistake to make is to use the auth state by navigating from another page, after it’s already settled, without handling the case where it hasn’t (another is imagining that the load won’t happen due to the auth check in the layout template … it will!). If navigating from another route everything will appear to work but fail if you refresh the page. Suddenly, the current user isn’t known (yet) so you have to wait for it.
But how? We can’t just loop until it is known as it would lock up the thread - what we need is a Promise that we can wait on.
This is the potential “gotcha”. You might decide to change your auth store so that instead of storing the state as undefined
/ object
/ null
, it can instead contain a Promise that will resolve to either object
or null
.
export const user = readable<Promise<User | null>>(
new Promise(_ => {}),
set => onAuthStateChanged(
auth, user => set(Promise.resolve(user))
)
)
So we’re good, right?
Unfortunately, the code doesn’t actually “contain a Promise that will resolve to either object
or null
”. Insted it contains an unresolved promise that will be replaced with a resolved promise when the auth state changes - a subtle but critical difference. Anytime it does change, it’s a new promise.
What does that mean?
Well, while our template can easily be adapted to use the promise:
{#await $user}
<Spinner />
{:then user}
{#if user}
<YourData />
{:else}
<AccessDenied />
{/if}
{/await}
That only works because the template responds to the store content itself changing - the promise being replaced. Although the await
block is in there, what actually causes the rendered content to change is the promise itself being changed to a resolved promise and the template re-evaluating it.
If we await the promise that the store contains, we’ll wait forever because that promise is never resolved:
THIS WON’T WORK
import type { PageLoad } from './$types'
import { user } from '$lib/firebase'
import { get } from 'svelte/store
export const load: PageLoad = async ({ params }) => {
const u = await get(user)
// fetch and return data based on user
}
The promise in our store doesn’t really help us or give us anything to await
so it’s simpler all round to just go back to the non-promise version.
If we do want to await for it being resolved, we need to create a promise and subscribe to the store inside that, only resolving the promise when the state is known.
This is the basic approach:
await new Promise<void>(resolve => {
let unsub = () => { }
unsub = user.subscribe(u => {
if (u !== undefined) {
resolve()
unsub() // unsubscribe once state known
}
})
})
Obviously, this could be put into a helper function that could be imported and called wherever required. But if we already have a store for auth we could make the promise into a property of it:
function createUserStore() {
const { subscribe } = readable<User | null>(
undefined,
set => onAuthStateChanged(auth, set)
)
const known = new Promise<void>(resolve => {
let unsub = () => { }
unsub = subscribe(user => {
if (user !== undefined) {
resolve()
unsub()
}
})
})
return { subscribe, known }
}
export const user = createUserStore()
We would just import our store as normal and can now await the promise that the auth state is then known:
import type { PageLoad } from './$types'
import { user } from '$lib/firebase'
import { get } from 'svelte/store
export const load: PageLoad = async ({ params }) => {
await user.known
// fetch and return data based on user
}
NOTE: although it’s possible to return the user from the promise, I don’t think it’s wise to do so as it might give the impression that it’s a reliable source of the user state, rather than just being the initial snapshot. The authoritative state always comes from the store itself which will reflect changes to it. So I prefer to keep the promise returning void
. We also don’t need to keep listening beyond the initial state change (it can never go back to undefined
AFAIK) so we unsubscribe once it is resolved.