採用はこちら!

Shinonome Tech Blog

株式会社Shinonomeの技術ブログ
7 min read

FirebaseとReactによる独立した認証管理(上)

Firebase Authenticationの認証情報を、Reactを用いてリアルタイムでアプリ全体に伝搬させる方法について紹介します。

はじめに

バックエンドの藤岡です。

今回はFirebaseを用いた認証情報のReactでの私なりの管理方法を備忘録としてまとめます。作成して長くなってしまったので二部作になります。今回はその上版です。

Firebase Authenticationでは簡単に強固な認証を実装することができます。また、FirestoreではNoSQLでリアルタイムでデータベースの情報をクライアントに共有することができます。このシリーズでは、Firebase Authenticationで認証をし、Firestoreにユーザー情報を保存しているという想定で、これら認証情報を他と独立して管理し、グローバルステートを用いてアプリ内に共有するやり方の一例を紹介します。今回は全体の構造説明とFirebaseから認証情報を取得して扱いやすく加工するところまでを扱います。

また、このやり方はあくまで私が色々試してきた中で良さそうと思ったものに過ぎませんので、その点予めご了承下さい。

前提知識

  • Firebase Authenticationの基礎知識
  • Firestoreの基礎知識
  • Reactの基礎知識

前提条件

  • Reactアプリの初期化
  • Firebaseの初期化
  • Firebase Authentication、FirestoreがReactアプリ内で使える状態にある

バージョン

  • React @18.2.0
  • Firebase @9.10.0
  • Typescript @4.8.2

本ドキュメントでは、React、Firebaseの基礎的な操作はある程度できることを前提で進めていきます。その点予めご了承下さい。Firebase AuthenticationやFirestoreのセットアップはこちらの公式ドキュメントをご参照下さい。

Firebase Authentication
Firebase Authentication lets you add an end-to-end identity solution to your app for easy user authentication, sign-in, and onboarding in just a few lines of code.
Get started with Cloud Firestore | Firebase

なお、本ドキュメントのsrcディレクトリは以下のようなディレクトリ構成を想定しています。

全体概要

今回の全体の概要は以下のようになっています。なお、紺色はコンポーネント、黄色はフックとなっています。

全体の流れとしては以下の通りです。

  1. Firebase AuthenticationやFirestoreから認証情報やユーザー情報を取得
  2. 取得した情報をグローバルステートに提供
  3. グローバルステートを共有するコンポーネントに提供

Firebase Authenticationから取得する認証情報やFirestoreから取得するスナップショットはリアルタイムで反映されるため、グローバルステートで取得する情報は常にリアルタイムの情報を反映することができます。

また、グローバルステートの取得をフック化することで、提供されているコンポーネント配下では、任意の場所でグローバルステートを呼び出すことができます。

今回は図の①の範囲を紹介します。それでは実際のコードを見ていきます。

認証情報の取得

今回は、Firebase Authenticationの認証情報をの onAuthStateChange() を使用して取得し、Firestoreに保存されているユーザー情報を onSnapshot() を使用して取得します。

認証情報

まず、認証情報について見ていきます。 onAuthStateChange() はその返り値にunsubscribe関数を返し、これが呼び出されるまで自動で認証情報を監視し続けます。そのため、onAuthStateChange() はアプリ立ち上げ時に一度だけ呼び出され、アプリが閉じられると同時にunsubscribe関数が呼び出されるようにします。そのために、 useEffectを使用します。

// ./src/hook/useObserveUser.tsx
  
useEffect(() => {
    const unsubscribed = onAuthStateChanged(auth, handleUser, handleError);
    return unsubscribed;
}, []);

useEffect の第二引数を [] とすることでコンポーネントが呼び出されたタイミングで一度だけコールバック関数が呼び出されるようにします。今回はアプリの最上位に置いているため、アプリが呼び出されたタイミングで一度だけ発火します。また、コールバック関数の返り値はコンポーネントが閉じられるタイミングに実行します。そのため、認証監視解除のunsubscribe関数を渡すことで、アプリが閉じるタイミングで監視を解除できます。

onAuthStateChange() の第一引数のauthは getAuth で取得した Auth 型の認証情報を入れて下さい。

第二引数には、認証情報が変化した際に実行されるコールバック関数を入れます。このコールバック関数は引数としてそのときの認証状態(サインイン状態なら User 、サインアウト状態なら null)を取ります。ここでは、サインインしたらユーザー情報をステートに保存するような処理がいいでしょう。

// ./src/hook/useObserveUser.tsx

const [user, setUser] = useState<User | null>(null);

// 正常にユーザーを取得できた場合の処理
const handleUser = (user: User | null) => {
setUser(user);
};

第三引数には第二引数のコールバック関数でエラーが起こったときに呼び出される関数が入ります。エラーが起きた場所を簡単に知りたい場合や、処理ごとでエラー処理を変えたい場合は第二引数にエラー処理を記述したほうがいいですが、コールバック関数内で非同期処理をあまりしないので、私はこの位置にエラー処理を記述することが多いです。

これでステートに入れた user を返せば認証情報をリアルタイムで監視するフックの完成です。usernull を返せばログアウト状態、 User 型を返せばログイン状態という具合です。少し手直ししてコード全体は以下のようになります。 isLoading については後述します。

// ./src/hook/useObserveUser.tsx
import { auth } from "../firebase/firebase";

// onAuthStateChangeを用いてユーザーを関しするhook
const useObserveUser = () => {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [error, setError] = useState<Error | null>(null);

  // 正常にユーザーを取得できた場合の処理
  const handleUser = (user: User | null) => {
    setUser(user);
    setIsLoading(false);
  };

  // ユーザーが取得できなかったときの処理
  const handleError = (error: Error) => {
    setError(error);
    setIsLoading(false);
  };

  useEffect(() => {
    setIsLoading(true);
    const unsubscribed = onAuthStateChanged(auth, handleUser, handleError);
    return unsubscribed;
  }, []);

  return { user, isLoading, error };
};

export default useObserveUser;

参考:onSnapshotについての公式ドキュメント

Manage Users in Firebase

ユーザー情報

次に、ユーザー情報について見ていきます。ユーザー情報についても概ね認証情報と同様です。onSnapshot() は第一引数に得られたFirestoreドキュメント参照やクエリを監視し続け、ドキュメントに更新があった場合に第二引数のコールバック関数が呼ばれます。そのため、ユーザー情報についてはユーザーがサインインしてきたタイミングで一度だけonSnapshot() を実行し、サインアウト及びアプリ終了時に監視解除関数を呼び出します。同様に useEffect を使用します。

  // ./src/hook/useObserveUserDoc.tsx
  import { db } from "../firebase/firebase";
  
  // ユーザードキュメントをオブサーブするエフェクト
  useEffect(() => {
    if (user) {
      // サインイン時
      const unsubscribe = onSnapshot(
        doc(db, "User", user.uid),
        handleDoc,
        handleError
      );
      return unsubscribe;
    } else {
      // サインアウト時
    }
  }, [user]);

user は先程のフックから取得したものを使用します。

onSnapthot の第一引数にはドキュメントの参照を渡します。 dbgetFirestore() で取得したものを使用します。

第二引数のコールバック関数は引数に監視対象のドキュメントのスナップショットを返します。ドキュメントが存在する場合にステートに保存するようにします。

第三引数にはエラー処理を書きます。認証情報のときと同じ理由で私はここによく書きます。

これでステートに入れた userDoc を返せばユーザー情報をリアルタイムで監視するフックの完成です。ログアウト状態なら userDocnull を返し、ログイン状態なら DocumnetData 型を返すという具合です。少し手直ししてコード全体は以下のようになります。 isLoading については後述します。

 // ./src/hook/useObserveUserDoc.tsx
import { User } from "firebase/auth";
import {
  doc,
  DocumentData,
  DocumentSnapshot,
  FirestoreError,
  onSnapshot,
} from "firebase/firestore";
import { useEffect, useState } from "react";
import { db } from "../firebase/firebase";

// onSnapshotを用いてユーザーのドキュメントを監視するhook
// ユーザーがいない場合はnullを返す
const useObserveUserDoc = (user: User | null) => {
  const [userDocData, setUserDocData] = useState<DocumentData | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [error, setError] = useState<Error | string | null>(null);

  // onSnapshotで正常にdocを取得できた場合の処理
  const handleDoc = (doc: DocumentSnapshot<DocumentData>) => {
    // もしドキュメントがちゃんとあれば
    if (doc.exists()) {
      const data = doc.data();
      setUserDocData(data);
    }
    // ドキュメントがなければ
    else {
      setError("ドキュメントがありません");
    }
    setIsLoading(false);
  };

  // onSnapshotでdocが取得できなかったときの処理
  const handleError = (error: FirestoreError) => {
    setError(error);
    console.log("onSnapshotでのエラーです", error);
    setIsLoading(false);
  };

  // ユーザードキュメントをオブサーブするエフェクト
  useEffect(() => {
    setIsLoading(true);
    if (user) {
      const unsubscribe = onSnapshot(
        doc(db, "User", user.uid),
        handleDoc,
        handleError
      );
      return unsubscribe;
    } else {
      setIsLoading(false);
      setUserDocData(null);
    }
  }, [user]);

  return { userDocData, isLoading, error };
};

export default useObserveUserDoc;

参考:onSnapshotの公式ドキュメント

Get realtime updates with Cloud Firestore | Firebase

データの統合

これにてFirebaseから認証情報やユーザー情報を取得するフックの完成です。使いやすいように認証関係のフックをまとめておきます。 useAuthStatus については以下の(余談)にて。

// ./src/hook/useInitUser.tsx
import { useMemo } from "react";

import useObserveUser from "./useObserveUser";
import useObserveUserDoc from "./useObserveUserDoc";
import useAuthStatus from "./useAuthStatus";
import { validateUserType } from "../function/validateUserType";

const useInitUser = () => {
  // Authenticationのユーザーを監視するhook
  const {
    user,
    isLoading: isAuthLoading,
    error: authError,
  } = useObserveUser();

  // ユーザーのドキュメントを監視するhook
  const {
    userDocData,
    isLoading: isDocLoading,
    error: docError,
    userDoc,
  } = useObserveUserDoc(user);

  // ユーザーの状態やタイプを判別するhook
  const { authStatus, error: userTypeError } = useAuthStatus(
    user,
    userDocData,
    isAuthLoading || isDocLoading
  );

  const error = useMemo(() => {
    if (user && userDocData && userTypeError) {
      validateUserType(user, userDocData);
    }
    return { authError, docError, userTypeError };
  }, [authError, docError, user, userDocData, userTypeError]);

  return { user, authStatus, userDocData, error };
};

export default useInitUser;

useInitUser フックの返り値はそれぞれリアルタイムで更新されます。そして、その値をステートに保存しているため、変更が生じるごとに該当コンポーネントが再レンダリングされます。そのため、useInitUser フックが返す認証情報などの返り値はすべてリアルタイムのものであることが保証されます。

なお、この”フックをまとめるフック”というものは、現在どの認証管理のフックを動かしているのかが分かりやすいという主観的な理由で作っているものですので、必要に応じて作り変えたり削除してみたりして下さい。

余談

今回は省略しますが、Firebase Authenticationのカスタムフックを用いてユーザータイプの管理のようなものもできます。上記のコード useAuthStatus フックがまさにそうです。ユーザータイプもリアルタイムで監視することにより、ユーザータイプごとで使用するUIを分けるのに重宝します。 isLoading はそのときにロード判定などで使えるでしょう。カスタムフックの扱い方や、ユーザータイプごとに振る舞いの違うアプリの作成については別の記事でまとめるかもしれません。(参考:カスタムフックについての公式ドキュメント

今回作成した useInitUser はアプリ立ち上げ時に一度だけ呼び出し、その影響を所定の範囲全体(今回はアプリ全体)に共有したいときに重宝します。加えたい機能があれば、それを扱うフックを作成し、ここで呼び出せばよいだけなので管理も楽かと思います。先に述べたユーザータイプの扱い以外にも、通知の管理やメンテナンス画面表示などでも使えます。

また、今回のユーザー情報を含んでいるユーザードキュメントは、Cloud Functionsを使用して、ユーザーがサインアップしたタイミングで自動的に作成されることを想定しています。このCloud Functionsを使ったサインアップの処理についても別の記事でまとめるかもしれません。閑話休題。

まとめ

今回はFirebase AuthenticationとFirestoreから提供される認証情報をReactの useEffect を用いて、これらデータを提供するフックを作成しました。もし今回紹介させていただいた内容がなにかの参考になれば幸いです。