Suspense-ready useAuth hook for firebase auth
'use client'
import { use, useSyncExternalStore } from 'react'
import './firebase/app'
import { getAuth } from 'firebase/auth'
const subscribe = (callback: () => void) => {
const auth = getAuth()
const unsubscribe = auth.onAuthStateChanged(callback)
return unsubscribe
}
const getSnapshot = () => {
const auth = getAuth()
return auth.currentUser
}
let ready: Promise<void>
if (typeof window !== 'undefined') {
ready = new Promise((resolve) => {
const unsubscribe = subscribe(() => {
unsubscribe()
resolve()
})
})
}
export const useAuth = () => {
if (typeof window === 'undefined') throw new Error('Browser only')
use(ready) // suspend until firebase auth is ready
const currentUser = useSyncExternalStore(subscribe, getSnapshot)
return { currentUser, isLoggedIn: currentUser !== null }
}
ポイントなど
firebase authから認証状態を読み取るのは「外部ストア」に当たるのでuseSyncExternalStoreを使う
"You Might Not Need an Effect"でもSubscribing to an external storeとしてuseSyncExternalStoreを使うことが推奨されています
このhookは元々reduxなどの状態管理ライブラリがReact 18のconcurrent rendering時にtearingというレンダリングの一貫性を破る問題を修正するために作られたものなので、useState&useEffectでcurrentUserを管理するより一貫性が担保されると思われます
currentUserが読み込まれるまでsuspendする
今までのように自前で管理していた場合はuseStateの初期値にundefinedを入れておくなどすれば、一度でも読み込まれたかが分かりましたが、auth.currentUserはそれらを区別せずに初期値がnullなので、読み込み状態を得るためにonAuthStateChangedが最初に呼び出されたときに解決するpromiseを作ってuse
してsuspendさせます[1][2]
firebaseのAPIがNode.jsランタイムなどで実行されるのを防ぐ
firebase js sdkはクライアント側(ブラウザ内)での実行を前提にしていて、サーバー側で実行すると警告が出たりすることもあるので、これを避けるために条件分岐を入れています
特にif (typeof window === 'undefined') throw new Error('Browser only')
は単なるassertionではなく、streaming server renderingしているときにエラーがthrowされたコンポーネントを直近のSuspenseまでサーバー側でのレンダリングをスキップするドキュメントにも載っているReactの動作を使っています[3]
残念ながらNext.jsでの開発時にはこのエラーが常に表示されてしまうようです。
bottom-upでsuspenseと協調して動作させるようにするためにはおそらくこの方法しかないので、Next.js側で何らかの対応を待つことになりそうです。