useSyncExternalStore を使った Firebase Authentication の状態管理
Firebase Authentication では onAuthStateChanged
を利用することで、ログインしているユーザーを取得することが出来ます。
useEffect を使った管理
今までは以下のように useEffect
を使ってオブザーバーからユーザーを取得し state に入れていました。
const [user, setUser] = useState<FirebaseUser | undefined>();
useEffect(() => {
const auth = getAuth(firebaseApp);
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
} else {
setUser(undefined);
}
});
}, [])
useSyncExternalStore を使った管理
しかし React 外部の状態を扱う useSyncExternalStore
が出来たので useEffect
を使わずに Firebase Authentication の状態管理を行うようにしました。
useSubscribeAuthStateChanged
をアプリケーションのルートあたり(Next.js であれば App あたり)に設置し AuthState を Context の Provider に渡します。
AuthState を使いたい場所では Context から値を取得しています。
import { User, getAuth, onAuthStateChanged } from "firebase/auth";
import { useState, useSyncExternalStore } from "react";
type AuthState = {
status: "loading" | "login" | "logout";
user: User | undefined;
};
export const useSubscribeAuthStateChanged = () => {
// FirebaseApp を取得する hook を別で実装しています
const app = useFirebaseApp();
// 状態を保持する store は useState の初期化処理で生成し state として保持しています
// store の再生成を行わないための処置で state の更新は行いません
// この useState を用いる方法は react-query で useSyncExternalStore を利用している部分を参考にしました
// https://github.com/TanStack/query/blob/845751180decd86b2feab669e487a2887e12f8b5/packages/react-query/src/useBaseQuery.ts#L68-L95
const [store] = useState(() => {
return getStore(app);
});
const state = useSyncExternalStore<AuthState>(
store.subscribe,
store.getSnapshot,
store.getServerSnapshot,
);
return state;
};
const initialState: AuthState = { status: "loading", user: undefined };
const getStore = (app: ReturnType<typeof useFirebaseApp>) => {
let state: AuthState = initialState;
return {
getSnapshot: () => state,
getServerSnapshot: () => initialState,
subscribe: (callback: () => void) => {
const auth = getAuth(app);
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
state = {
status: "login",
user: user,
};
} else {
state = {
status: "logout",
user: undefined,
};
}
callback();
});
return () => {
unsubscribe();
};
},
};
};
useSyncExternalStore
で扱う状態の保持は以下の state に持たせています。
この useState を用いる方法は react-query で useSyncExternalStore
を利用している部分を参考にしました。
const [store] = useState(() => {
return getStore(app);
});
getStore
関数は、内部に変数 state
を持たせており、この値を useSyncExternalStore
の snapshot として渡しています。
const getStore = (app: ReturnType<typeof useFirebaseApp>) => {
let state: AuthState = initialState;
// 略
}
感想
getStore
関数を用意せず、単純に let state: AuthState
を変数として用意しても、おそらく問題はありません。
ただ、以下のように変数を用意して取り回す作りは、グローバル変数みたいで個人的に好みでは無かったため(Server-side Renderingで状態が共有されるようなバグを仕込んでしまいそう)、今回の useState
の初期化処理で生成し、更新しない state に持たせる方法を取りました。
// auth.ts
let state: AuthState = initialState;
export const useSubscribeAuthStateChanged = () => {/* 略 */};
Discussion