Captain Codeman Captain Codeman

How to await Firebase Auth with SvelteKit

Waiting for auth to settle outside of templates

Contents

Firebase Auth State Needs To Settle

Using Firebase Authentication involves 3 steps:

  1. Add the Firebase JS SDK to your project
  2. Initialize a Firebase App instance on the client
  3. Get a reference to the Firebase Auth service

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?

Awaiting Auth State is Easy in Svelte Templates

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.

Awaiting in SvelteKit load Functions

But 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
}

Creating an awaitable Auth Promise

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.