ReactのCustom HookでFirebaseの認証機能を実装

公開 2022年02月03日更新 2022年02月03日

React.jsTypeScriptFirebaseAuth

Firebaseの勉強のため、Firebase Authenticationで認証機能を実装します。
メールアドレスとパスワードで認証します。

使用技術

  • Firebase Authentication
  • React.js
  • TypeScript

1. Reactプロジェクトを作成する

私は自分用のReactテンプレートがあるので、それを使います。
しかしcreate-react-appでも問題なく手順を実行できると思います。

npx create-react-app firebase-auth-practice --template typescript

 
プロジェクトルートで以下を実行してFirebaseをインストールします。

npm i firebase

2. Firebaseプロジェクトを作成する

Firebaseのコンソールにアクセスします。
https://console.firebase.google.com/

「プロジェクトを追加」をクリックします。
01

プロジェクト名を入力して「続行」を押下します。
02

「続行」を押下します。
03

Google アナリティクスのアカウントを選択してから「プロジェクトを作成」を押下します。
※初回はアカウントがないので、作成します
04

待ちます。
05

完了したら「続行」を押下します。
06

「ウェブ」を押下します。
07

アプリのニックネームを適当に入力してから「アプリを登録」を押下します。
08

「コンソールに進む」を押下します。
09

コンソールに戻るので、左のドロワーの「Authentication」を選択します。
10

「始める」を押下します。
11

「メール/パスワード」を選択します。
12

「メール/パスワード」を有効にします。
「メールリンク(パスワードなしでログイン)」は使いたい場合は有効にします。(後から設定可能)
「保存」を押下します。
13

左のドロワーの「プロジェクトの設定」を選択します。
14

下にスクロールして、JavaScriptのコードをコピーします。
15

3. Reactプロジェクトに認証機能を追加

src/index.tsxに先程コピーしたコードを貼り付けます。

// src/index.tsx
import { getAnalytics } from "firebase/analytics";
import { initializeApp } from "firebase/app";
import React from "react";
import ReactDOM from "react-dom";

const firebaseConfig = {
    apiKey: "AIzaSyATzDcdnsLQXWLEIittbRgtu_sRUfEZXgQ",
    authDomain: "auth-practice-5fbad.firebaseapp.com",
    projectId: "auth-practice-5fbad",
    storageBucket: "auth-practice-5fbad.appspot.com",
    messagingSenderId: "762238832983",
    appId: "1:762238832983:web:b36f3fa07673ba6f896e12",
    measurementId: "G-SJSN6K80KR",
};

const app = initializeApp(firebaseConfig);
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
const analytics = getAnalytics(app);

ReactDOM.render(<div></div>, document.getElementById("root"));

カスタムフックで認証ロジックを実装します。
src/hooksディレクトリを作成し、そこにauth.tsxを以下のように実装します。

// src/hooks/auth.tsx
import React, { useEffect, useState, useContext, createContext } from "react";
import {
    getAuth,
    createUserWithEmailAndPassword,
    onAuthStateChanged,
} from "firebase/auth";
import type { User } from "firebase/auth";

interface UseProvideAuth {
    isFirstLoading: boolean;
    isAuthenticated: boolean;
    user: User | null;
    signUp: (email: string, password: string) => Promise<boolean>;
}

const authContext = createContext({} as UseProvideAuth);

export const ProvideAuth: React.FC = ({ children }) => {
    const auth = useProvideAuth();
    return <authContext.Provider value={auth}>{children}</authContext.Provider>;
};

export const useAuth = () => {
    return useContext(authContext);
};

const useProvideAuth = (): UseProvideAuth => {
    const [isFirstLoading, setIsFirstLoading] = useState(true);
    const [isAuthenticated, setIsAuthenticated] = useState(false);
    const [user, setUser] = useState<User | null>(null);

    // アプリケーションの最初のレンダリング直後に認証を確認するロジック
    useEffect(() => {
        const auth = getAuth();
        onAuthStateChanged(auth, (user) => {
            if (user) {
                setUser(user);
                setIsAuthenticated(true);
            } else {
                setIsAuthenticated(false);
            }
            setIsFirstLoading(false);
        });
    }, []);

    const signUp = async (email: string, password: string) => {
        const auth = getAuth();
        // サインアップ処理
        return createUserWithEmailAndPassword(auth, email, password)
            .then((userCredential) => {
                setUser(userCredential.user);
                setIsAuthenticated(true);
                return true;
            })
            .catch((error) => {
                const errorCode = error.code;
                const errorMessage = error.message;
                console.log(errorCode);
                console.log(errorMessage);
                setIsAuthenticated(false);
                return false;
            });
    };

    return {
        isFirstLoading,
        isAuthenticated,
        user,
        signUp,
    };
};

src/App.tsxから先程のhookを実行して登録フォームを実装します。

// src/App.tsx
import React, { useState } from "react";
import { useAuth } from "./hooks/auth";

export const App = () => {
    const auth = useAuth();
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    if (auth.isFirstLoading) {
        return <div>check authentication...</div>;
    }

    const signUp = () => {
        auth.signUp(email, password)
            .then(() => {
                console.log("登録に成功しました");
            })
            .catch(() => {
                console.log("登録に失敗しました...");
            })
            .finally(() => {
                setEmail("");
                setPassword("");
            });
    };

    return (
        <div>
            <p>
                {auth.isAuthenticated
                    ? "ログイン中です"
                    : "ログインしていません"}
            </p>
            <p>
                mail
                <input
                    type="email"
                    name="email"
                    id="email"
                    value={email}
                    onChange={(e) => {
                        setEmail(e.target.value);
                    }}
                />
            </p>
            <p>
                password
                <input
                    type="password"
                    name="password"
                    id="password"
                    value={password}
                    onChange={(e) => {
                        setPassword(e.target.value);
                    }}
                />
            </p>
            <input type="button" value="会員登録" onClick={signUp} />
        </div>
    );
};

src/index.tsxを以下のように修正して、hookをアプリケーション全体に反映します。

import { getAnalytics } from "firebase/analytics";
import { initializeApp } from "firebase/app";
import React from "react";
import ReactDOM from "react-dom";
// 追加
import { App } from "./App";
import { ProvideAuth } from "./hooks/auth";

const firebaseConfig = {
    apiKey: "AIzaSyATzDcdnsLQXWLEIittbRgtu_sRUfEZXgQ",
    authDomain: "auth-practice-5fbad.firebaseapp.com",
    projectId: "auth-practice-5fbad",
    storageBucket: "auth-practice-5fbad.appspot.com",
    messagingSenderId: "762238832983",
    appId: "1:762238832983:web:b36f3fa07673ba6f896e12",
    measurementId: "G-SJSN6K80KR",
};

const app = initializeApp(firebaseConfig);
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
const analytics = getAnalytics(app);

ReactDOM.render(
    // 修正
    <ProvideAuth>
        <App />
    </ProvideAuth>,
    document.getElementById("root")
);

これで実装は完了です。
以下で動作確認します。

npm run dev

登録前
16

登録後
17

FirebaseコンソールのAuthenticationのUsersタブを見ると、ユーザーが登録されていることが確認できます。
18

【疑問】firebaseConfigは公開しても良いのか?

AWS Amplifyでは、このようなメタ情報はgitignoreにして公開しなかったので、Firebaseでは公開して大丈夫なのか気になりました。
調べてみると、どうやら基本的に公開して問題ないようです。
https://firebase.google.com/docs/projects/api-keys#api-keys-for-firebase-are-different

上記リンクからの引用

FirebaseサービスのAPIキーをシークレットとして扱う必要はありませんが、APIキーの誤用からプロジェクトを保護するために、追加の対策を講じる必要がある特定のケースがいくつかあります。

stackoverflowにもこれについての質問がありました。
https://stackoverflow.com/questions/37482366/is-it-safe-to-expose-firebase-apikey-to-the-public

まとめ

とりあえず登録機能だけ実装しましたが、ログインやログアウトなども同じようにsrc/hooks/auth.tsxに実装すれば良いです。
Firebaseは初めて使いましたが、短時間で認証機能を導入でき、コンソールのUIも見やすくて良いサービスだと思いました。
今後はソーシャルアカウント認証やゲスト認証なども実装するつもりなので、この記事に追記するかもしれません。

最後までお読みいただきありがとうございました!

参考リンク

Firebaseドキュメント
FirebaseをJavaScriptプロジェクトに追加する
ウェブサイトでFirebase Authenticationを使ってみる