本文書ではReact 18環境でのFirebase v9を使った認証について説明を行っています。Firebaseの認証機能を利用することでユーザ情報を保存するための独自のデータベースなどを設定する必要はなく短時間で簡単にReactに認証機能を実装することができます。

Firebaseの認証ではメールアドレスとパスワード以外にGoogleアカウントなどさまざまなプロバイダーを利用した認証も行うことができます。本文書ではメールアドレスでの方法について説明しています。

Firebase上でプロジェクトの作成など各種設定を行う必要がありますがが、プロジェクトの作成、認証の設定、メソッドはReact固有の設定ではなくVue.jsを含め他のフレームワークでも利用することができます。

本文書では認証を実装するためにReactの下記の機能を利用しているのでFirebase認証設定を理解するのと同時にReactの使い方も同時に学ぶことができます。本ブログでは各機能の基本的な説明を公開しているので参考にしてください。

  • React Router
  • useEffect
  • useState
  • useContext
  • useRef

ReactとFirebase、React Routerの古いバージョンを利用した設定方法は以下で公開済みです。

Reactプロジェクトの作成

macOS上でReactプロジェクトを作成するためにNode.jsのインストールが必要となります。Node.jsインストールを行ってからReactプロジェクトの作成を行ってください。

Node.jsはJavaScriptのコードを動かすために必要です。
fukidashi

npx create-react-appコマンドを利用してReactプロジェクトを作成します。コマンドの後ろにはプロジェクト名を指定します。プロジェクト名には任意の名前をつけてください。ここではreact-fierebase-authとしています。


 % npx create-react-app react-firebase-auth

実行するとプロジェクト名に指定した名前のフォルダが作成されます。

Firebaseの初期設定

FirebaseのAuthenticationの機能を利用するためにはFirebaseでユーザアカウント登録を行う必要があります。もしアカウントを持っていない場合は、Firabaseのページでユーザ登録を行なってください。Googleのアカウントが必要となりますがクレジットカード等の情報を入力する必要はありません。

Firebaseのアカウントを取得後にFirebaseにアクセスすると以下の画面が表示されます。Firebaseの機能を利用するためには最初にプロジェクトの作成が必要となります。画面中央にある”プロジェクトを作成(Create a project)”ボタンをクリックしてください。

Firebase Welcomeページ
Firebase Welcomeページ

プロジェクトの名前をつける画面が表示されるので任意の名前をつけてください。ここではreact-authという名前をつけています。

プロジェクト名の設定
プロジェクト名の設定

このプロジェクトでGoogleアナリティクスを有効にするがONになっていますが本文書では動作確認なので利用しないためOFFに設定して”プロジェクトを作成”ボタンをクリックしてください。

プロジェクトの作成画面2
プロジェクトの作成画面2

“プロジェクトを作成”ボタンをクリックしてプロジェクトの作成が完了すると概要ページが表示されます。

プロジェクトの概要ページ
プロジェクトの概要ページ

ReactからFirebaseのサービスに接続するための認証情報が必要になるのでプロジェクトの概要画面の左から3番目のボタンをクリックしてアプリの登録を行います。

アプリの追加
アプリの追加

アプリの登録を行うためニックネームの設定を行う必要があります。任意の名前をつけてください。ここではreact-authというニックネームを設定しています。設定したら”アプリ登録”ボタンをクリックしてください。

アプリのニックネームの登録
アプリのニックネームの登録

”アプリの登録”ボタンをクリックするとFirebaseに接続するための情報が表示されます。

Firebase SDKの追加
Firebase SDKの追加

表示されている情報はReactで環境変数として利用するため、作成したReactプロジェクトフォルダの直下に.env.localファイルを作成してください。

開発環境であれば.env.local以外にも.env, .env.local.development, env.developmentなどのファイルを利用することがあります。ファイル名によって優先度が変わるので気になる人はReactのドキュメントの”Adding Custom Environment Variables”を確認してください。
fukidashi

.env.localファイルに環境変数を設定する場合は先頭(プレフィックス)にREACT_APP_をつける必要があります。Firebaseのページに表示されている値を下記のように設定します。

各自の取得した値を設定してください。下記は利用することはできません。


REACT_APP_FIREBASE_API_KEY="AIzaSyDA3RPryZv-CfKanopj3eYu2Is-7A73nYE"
REACT_APP_FIREBASE_AUTH_DOMAIN= "react-auth-990.firebaseapp.com""
REACT_APP_FIREBASE_PROJECT_ID="react-auth-990"
REACT_APP_FIREBASE_STORAGE_BUCKET="react-auth-990.appspot.com"
REACT_APP_FIREBASE_MESSAGE_SENDER_ID="630715041250"
REACT_APP_FIREBASE_APP_ID="1:6337050413950:web:a3af7030ed4e782145340f"

環境変数の設定が完了したらFirebase接続のための設定ファイルfirebase.jsをsrcフォルダの下に作成します。


import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGE_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_SENDER_ID,
};

initializeApp(firebaseConfig);

export const auth = getAuth();

.env.localファイルの環境変数はprocess.env.環境変数名で取得することが可能です。

firebase.jsファイルでは、firebaseに接続するために必要なinitializeAppと認証に必要なgetAuthをimportしています。firebaseをimportしていますがReactにデフォルトから含まれているわけではないのでfirebaseパッケージのインストールが必要となります。プロジェクトフォルダ直下でnpmコマンドを使ってfirebaseパッケージをインストールしてください。


 % cd react-firebase-auth
 % npm install firebase

firebaseパッケージをインストールした直後のpackage.jsonは以下の通りです。


{
  "name": "react-firebase-auth",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.14.1",
    "@testing-library/react": "^13.0.0",
    "@testing-library/user-event": "^13.2.1",
    "firebase": "^9.10.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.0"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Firebaseの認証の設定

Firebaseの認証(Authentication)の設定を行います。プロジェクトの概要ページから中央にあるAuthentication(ユーザの認証と管理)をクリックしてください。

プロジェクトの概要から認証をクリック
プロジェクトの概要から認証をクリック

Authenticationのページが表示されるので”始まる”ボタンをクリックしてください。

Authenticationページ
Authenticationページ

ユーザがサインイン(ログイン)するために利用することができるログインプロバイダーの一覧が表示されます。Googleアカウントなども利用することができますがここでは一番左上のネイティブのプロバイダの”メール/パスワード”を利用します。”メール/パスワード”をクリックしてください。

サインイン方法の一覧
サインイン方法の一覧

メール/パスワード、メールリンクどちらも無効になっているのでメール/パスワードを”有効にする”に変更して保存ボタンをクリックしてください。

メール/パスワードでの設定
メール/パスワードでの設定

保存後はメール/パスワードのステータスのみ”有効”になっていることが確認できます。

ステータスの確認
ステータスの確認

ここまででFirebaseの設定は完了です。

環境変数とfirebaseの設定ファイルの作成などを行いましたがここまでの作業はReactに限らず他のフレームワークを利用した場合でも実行する共通作業です。
fukidashi

ここからはReact側の設定に入っていきます。

ユーザ登録画面(サインアップ画面)の作成

Firebaseへのユーザ登録が行えるか確認するために最初にユーザ登録画面の作成を行います。

ReactにおけるFirebaseでの認証に注目しているためユーザ登録画面、ユーザログイン画面など最低限のスタイル(CSS)を適用しています。
fukidashi

componentsフォルダを作成してSignUp.jsファイルを作成します。


const SignUp = () => {
  return <h1>ユーザ登録</h1>;
};

export default SignUp;

SignIn.jsファイルが作成できたらsrc直下にあるApp.jsファイルを以下のように更新します。App.jsファイルからSignUpコンポーネントをimportしています。

SignUp.jsファイルが作成できたらsrc直下にあるApp.jsファイルを以下のように更新します。App.jsファイルからSignUpコンポーネントをimportしています。


import SignUp from './components/SignUp';

function App() {
  return (
    <div style={{ margin: '2em' }}>
      <SignUp />
    </div>
  );
}

export default App;

App.jsファイルを更新したらnpm startコマンドで開発サーバを起動してください。自動でブラウザが起動します。表示している内容を確認するとSignUp.jsファイルに記述したユーザ登録画面が表示されていることがわかります。

ユーザ登録画面
ユーザ登録画面

SignUpコンポーネントにユーザ登録フォームを作成します。登録フォームは通常のHTML文と同じです。登録ボタンをクリックするとhandleSubmit関数が実行されます。handleSubmitの中ではevent.preventDefault()でsubmitイベントのデフォルトの動作を停止しています。preventDefaultがない場合登録ボタンをクリックすると画面がリロードされます。


const SignUp = () => {
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log('登録');
  };

  return (
    <div>
      <h1>ユーザ登録</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" name="password" type="password" />
        </div>
        <div>
          <button>登録</button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

ブラウザで確認すると設定した入力フォームが表示されます。登録ボタンをクリックするとブラウザのデベロッパーツールのコンソールに”登録”というメッセージが表示されます。

ユーザ登録フォームの動作確認
ユーザ登録フォームの動作確認

input要素の値の取得

ユーザを登録するためにinput要素に入力した値を取得してFirebaseに送信する必要があります。入力した値を取得する方法は複数存在あるのでその中から3つ簡単に説明しておきます。3つ方法を説明していますがこの後利用するのは最もコードが短い最初の方法で、入力した値をeventから取得を利用して認証機能を実装していきます。console.logで入力した値を表示できるようにしているので本当に取得できるか確認しておいてください。

eventから取得

下記のようにeventのtarget.elementsを使って入力した値を取得することができます。


const handleSubmit = (event) => {
  event.preventDefault();
  const { email, password } = event.target.elements;
  console.log(email.value, password.value);
};

useRefを利用

React HookのuseRefを利用することで直接input要素から値を取得することができます。


import { useRef } from 'react';
const SignUp = () => {
  const emailRef = useRef(null);
  const emailPassword = useRef(null);
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(emailRef.current.value, emailPassword.current.value);
  };

  return (
    <div>
      <h1>ユーザ登録</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label>メールアドレス</label>
          <input name="email" type="email" placeholder="email" ref={emailRef} />
        </div>
        <div>
          <label>パスワード</label>
          <input
            name="password"
            type="password"
            placeholder="password"
            ref={emailPassword}
          />
        </div>
        <div>
          <button>登録</button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

useStateを利用

React HookのuseStateを利用してinput要素に入力した値を変数に保存してhandleSubmit実行時に保存した値を利用します。


import { useState } from 'react';
const SignUp = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(email, password);
  };
  const handleChangeEmail = (event) => {
    setEmail(event.currentTarget.value);
  };
  const handleChangePassword = (event) => {
    setPassword(event.currentTarget.value);
  };

  return (
    <div>
      <h1>ユーザ登録</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label>メールアドレス</label>
          <input
            name="email"
            type="email"
            placeholder="email"
            onChange={(event) => handleChangeEmail(event)}
          />
        </div>
        <div>
          <label>パスワード</label>
          <input
            name="password"
            type="password"
            placeholder="password"
            onChange={(event) => handleChangePassword(event)}
          />
        </div>
        <div>
          <button>登録</button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

Firebaseへのユーザの登録

フォームに入力した値を取得できることが確認できたのでfirebaseにユーザを登録する処理を追加します。ユーザの登録にはfirebase/authのcreateUserWithEmailAndPasswordメソッドを利用します。

作成済のfirebase.jsファイルでexportしているauthをSignUpコンポーネントでimportして利用します。createUserWithEmailAndPasswordメソッドの引数にはauthと入力フォームから取得したメールアドレスとパスワードを指定します。


import { auth } from '../firebase';
import { createUserWithEmailAndPassword } from 'firebase/auth';

const SignUp = () => {
  const handleSubmit = (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    createUserWithEmailAndPassword(auth, email.value, password.value);
  };

  return (
    <div>
      <h1>ユーザ登録</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" name="password" type="password" />
        </div>
        <div>
          <button>登録</button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

設定が完了したらユーザ登録画面でemailとパスワードを入力して登録ボタンをクリックします。登録後の処理は何も行っていないので登録ボタンを押しても何も変化はありません。

メールアドレスとパスワードの入力
メールアドレスとパスワードの入力

登録ボタンをクリックした後、Firebaseの管理画面にアクセスを行いAuthenticationのページから”Users”のタブをクリックしてください。入力フォームで入力したユーザが登録されていることを確認してください。

firebase上のユーザ確認
Firebase上のユーザ確認

ここまでの設定でFirebaseへのユーザ登録の方法を理解することができました。

ユーザ情報の共有(Context)

ユーザ登録が完了後は登録したユーザが現在ログインしているかどうかの情報をアプリケーション内で保持する必要があります。すべてのコンポーネントでユーザ情報を共有するためにReact Hookの一つuseContextを利用します。useContextの基本的な設定方法について下記の文書で公開しているので参考にしてください。

srcフォルダの下にcontextフォルダを作成、AuthContext.jsファイルを作成します。ユーザ情報を含むuserを共有することが可能となります。下記のコードを見ただけで何が行われているか理解するのは難しいかと思いますがContextを利用する際のテンプレートみたいなものだと考えてください。


import { createContext, useState, useContext } from 'react';

const AuthContext = createContext();

export function useAuthContext() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState('');

  const value = {
    user,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;

AuthContext.jsファイルの中でユーザがサインイン、サインアウトを監視するメソッドonAuthStateChangedを設定します。FirebaseではリスナーとしてonAuthStateChangedはサインイン、サインアウトが行われると実行され、サインインした場合はuserオブジェクトにuserに関する値を持ちます。サインアウトした場合はnullとなります。

onAuthStateChangedメソッドの書式は下記の通りです。


import { getAuth, onAuthStateChanged } from "firebase/auth";

const auth = getAuth();
onAuthStateChanged(auth, (user) => {
  if (user) {
    // User is signed in, see docs for a list of available properties
    // https://firebase.google.com/docs/reference/js/firebase.User
    const uid = user.uid;
    // ...
  } else {
    // User is signed out
    // ...
  }
});

サインインした場合はuserに値が含まれるので、onAuthStateChangedとsetUserを組み合わせることでユーザがサインインした時のみユーザ情報を保存することができます。React HookのuseEffect Hookを利用し、マウント時にonAuthStateChangedを実行しサインイン/サインアウトを監視します。useEffectの中の処理はAuthcontext.jsのマウント時に1度だけ実行させるために[]は忘れずに設定を行なってください。アンマウント時はリスナーとして監視が必要なくなるため削除できるようonAuthStateChanged実行時に戻されるUnsubscribeを実行します。削除しないとアンマウントしてもリスナーとして処理を継続します。authはfirebase.jsからimportして利用します。


import { createContext, useState, useContext, useEffect } from 'react';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';

const AuthContext = createContext();

export function useAuthContext() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState('');

  const value = {
    user,
  };

  useEffect(() => {
    const unsubscribed = onAuthStateChanged(auth, (user) => {
      console.log(user);
      setUser(user);
    });
    return () => {
      unsubscribed();
    };
  }, []);

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
アンマウント時に行うリスナーの削除処理はreturn unsubscribedと記述することができます。()はつけない。
fukidashi

Contextを設定したらデータを共有するコンポーネントを作成したAuthProviderでラップする必要があります。


import SignUp from './components/SignUp';
import { AuthProvider } from './context/AuthContext';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <SignUp />
      </div>
    </AuthProvider>
  );
}

export default App;

App.jsファイルにAuthContextをimportとしてAuthProviderでラップします。ページをリフレッシュしてもonAuthStateChangedが実行されるのでconsole.logを使ってuserオブジェクトにアクセスできるか確認します。


useEffect(() => {
  const unsubscribed = auth.onAuthStateChanged((user) => {
    console.log(user);
    setUser(user);
  });
  return () => {
    unsubscribed();
  };
}, []);

先ほどユーザ登録を行っているのでユーザがログイン状態になっているはずです。

コンソールログを見るとuserに含まれる情報が表示されます。その中にemailなどの情報も確認することができます。

userオブジェクトの確認
userオブジェクトの確認

Contextの設定が完了しているのでSignupコンポーネントからもuserにアクセスすることが可能となっているのでSignupコンポーネントでuserにアクセスできるか確認してみましょう。AuthContextからuseAuthContextをimportしてuserを取得します。取得したuserからemailをブラウザ上に表示させています。


import { auth } from '../firebase';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { useAuthContext } from '../context/AuthContext';

const SignUp = () => {
  const { user } = useAuthContext();
  const handleSubmit = (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    createUserWithEmailAndPassword(auth, email.value, password.value);
  };

  return (
    <div>
      <h1>ユーザ登録 {user.email}</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input id="password" name="password" type="password" placeholder="password" />
        </div>
        <div>
          <button>登録</button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

登録したユーザのメールアドレスが表示されます。メールアドレスの確認ができたらuserを表示する必要はないのuse.emailとuseAuthContextに関するコードはSignup.jsファイルから削除してください。

メールアドレスを表示
メールアドレスを表示

ページをリロードしてもログインしているユーザのメールアドレスが表示されるはずです。一体どこに認証情報は保存されているのでしょう?

デベロッパーツールのApplication タブからStorageのIndexedDBのfirebaseLocalStorageを確認してください。emailなどの情報をここでも確認することができます。この値を削除するとユーザはログイン状態ではなくなります。まだここでは削除しないでください。ログアウト、ログイン機能を実装後に削除して動作確認を行ってみてください。

localStorageにfirebaseの認証情報
localStorageにfirebaseの認証情報

React Routerの設定

ユーザ登録の画面の設定を行いましたが認証機能を持つアプリケーションを構築する場合はユーザ登録ページだけではなくログインページやログイン完了後のページも作成する必要があります。複数のページを持つシングルページアプリケーションを構築するためにReact Routerが必要となります。

React Routerの基礎については下記で公開しているので参考にしてください。本文書はv6.4.1を利用しています。

React Routerを利用するためreact-router-dom@6ライブラリをインストールします。


 % npm install react-router-dom@6

インストールが完了したらRouterの設定を行っていきます。react-router-domからBrowserRouterとRoutesとRouterをimportしてます。/signupにアクセスがあったらSignUpコンポーネントが表示されるように設定を行います。


import SignUp from './components/SignUp';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Routes>
            <Route path="/signup" element={<SignUp />} />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

設定後は/(ルート)にアクセスしても画面には何も表示されません。ブラウザから/signupにアクセスを行ってください。ユーザ登録画面が表示されればルーティングの設定は正常に行われています。

ルーター設定後のsignup
ルーター設定後のsignup

新たにHome, Loginコンポーネントを作成するためにcomponentsフォルダにHome.jsとLogin.jsファイルを作成します。

それぞれ下記の内容を記述します。


const Home = () => {
  return <h1>ホームページ</h1>;
};

export default Home;

const Login = () => {
  return <h1>ログイン画面</h1>;
};

export default Login;

App.jsに追加したコンポーネントのルーティングを追加します。


import Home from './components/Home';
import SignUp from './components/SignUp';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/signup" element={<SignUp />} />
            <Route path="/login" element={<Login />} />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

Home, Loginコンポーネントを追加後は”/”、”/login”ページにアクセスして各ページに記述した内容が表示されるか確認を行なってください。

ログインページの作成

ユーザ登録ページの内容を元をログインページの作成を行います。ユーザ登録ページではhandleSubmitメソッドでcreateUserWithEmailAndPasswordメソッドを使っていましたが、ログイン時にはsignInWithEmailAndPasswordメソッドを利用します。ユーザ登録が行われていない場合は/signupに移動できるようにLinkコンポーネントをimportして設定を行なっています。


import { auth } from '../firebase';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { Link } from 'react-router-dom';

const Login = () => {
  const handleSubmit = (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    signInWithEmailAndPassword(auth, email.value, password.value);
  };

  return (
    <div>
      <h1>ログイン</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            type="password"
            placeholder="password"
          />
        </div>
        <div>
          <button>ログイン</button>
        </div>
        <div>
          ユーザ登録は<Link to={'/signup'}>こちら</Link>から
        </div>
      </form>
    </div>
  );
};

export default Login;

/loginにアクセスすると下記のページが表示されます。

ログインページ
ログインページ

Homeページの作成

Homeページは後ほどログインができたユーザのみアクセスができるように設定を行いますが、ホームページでログアウトができるように設定を行っておきます。firabase.jsからauthをimportしfirebase/authからsignOutをimportしています。


import { auth } from '../firebase';
import { signOut } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';

const Home = () => {
  const navigate = useNavigate();
  const handleLogout = () => {
    signOut(auth);
    navigate('/login');
  };
  return (
    <div>
      <h1>ホームページ</h1>
      <button onClick={handleLogout}>ログアウト</button>
    </div>
  );
};

export default Home;

設定後ログアウトボタンを押すとloginページにリダイレクトされることを確認してください。

ログイン画面は先ほど作成しているのでログインを行うことが可能です。しかしログイン後の処理は何も設定していないのでログインが完了してもブラウザには変化はありません。

先ほど説明した通りログインできたかどうかはIndexedDBのfirebaseLocalStorageを見ても確認することできます。ログアウトした時にキーが削除される等も確認をしてください。

保存されたキーの値を確認
保存されたキーの値を確認
ログアウトした場合にキーと値が表示されている場合はfirebaseLocalStorageDbの更新を行ってみてください。右クリックするとIndexedDBを更新が表示されます。
fukidashi

アクセスの制限設定

3つルーティングを追加しましたがここまで設定ではユーザがログインしているかどうかにかかわらずすべてのページにアクセスすることができます。Homeコンポーネントについてはログインしているユーザのみアクセスできるように設定を行います。

本文書では2つの方法で説明を行います。

Homeコンポーネントで分岐を利用

最初の方法はHomeコンポーネントの中で分岐を利用しログインを行っていない場合にはログインページにリダイレクトされるように設定を行なっています。

共有されているuserオブジェクトに対してHomeコンポーネントからアクセスを行い、userオブジェクトが値を持っていればHomeコンポーネントを表示し、持っていなければLoginコンポーネントにリダイレクトしています。if文の分岐を利用しているだけです。


import { auth } from '../firebase';
import { signOut } from 'firebase/auth';
import { useNavigate, Navigate } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';

const Home = () => {
  const navigate = useNavigate();
  const { user } = useAuthContext();
  const handleLogout = () => {
    signOut(auth);
    navigate('/login');
  };
  if (!user) {
    return <Navigate to="/login" />;
  } else {
    return (
      <div>
        <h1>ホームページ</h1>
        <button onClick={handleLogout}>ログアウト</button>
      </div>
    );
  }
};

export default Home;

ユーザがログインしている状態で/(ルート)にアクセスを行なってください。ログインしているにも関わらず/loginにリダイレクトされてしまいます。

アクセスを行うとAuthContext.jsのuseEffectでonAuthStateChangedによってユーザがログインしているかどうかチェックが行われます。しかしonAuthStateChangedによるチェックが完了していない状態でHomeコンポーネントがマウントされるためuserには値が入っておらずリダイレクトされます。この問題を防ぐためにはuserに値が入るまでHomeコンポーネントの表示を待たせる必要があります。

AuthContextコンポーネントにloading変数を追加します。デフォルト時にはloadingをtureに設定し、userに値が入った直後にloadingの値をfalseに設定します。この値がfalseになるまで表示させないように設定を行います。


import { createContext, useState, useContext, useEffect } from 'react';
import { auth } from '../firebase';
import { onAuthStateChanged } from 'firebase/auth';

const AuthContext = createContext();

export function useAuthContext() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState('');
  const [loading, setLoading] = useState(true);

  const value = {
    user,
    loading,
  };

  useEffect(() => {
    const unsubscribed = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });
    return () => {
      unsubscribed();
    };
  }, []);

  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

loadingを設定後にログインした状態で/(ルート)にアクセスを行なってください。これまでに比較して表示までに時間がかかりますがログインしている場合のみホームページが表示されます。

ログインしている場合のみ表示
ログインしている場合のみ表示

ログアウトボタンでログアウトしてから再度/(ルート)にアクセスを行ってください。/loginにリダイレクトされます。シンプルな方法ですがアクセス制限を行うことができました。


if (loading) {
  return <p>loading...</p>;
} else {
  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

Homeコンポーネント以外にもアクセス制限を行いたい場合、制限を行いたいコンポーネントで分岐の処理を毎回記述するのは非効率です。次はPrivateRouteコンポーネントを作成してアクセス制限を行う処理をHomeコンポーネントではなくPrivateRouteコンポーネントで行います。

Homeコンポーネントで行っていた分岐の処理を削除します。


import { auth } from '../firebase';
import { signOut } from 'firebase/auth';
import { useNavigate } from 'react-router-dom';

const Home = () => {
  const navigate = useNavigate();
  const handleLogout = () => {
    signOut(auth);
    navigate('/login');
  };

  return (
    <div>
      <h1>ホームページ</h1>
      <button onClick={handleLogout}>ログアウト</button>
    </div>
  );
};

export default Home;

PrivateRouteを利用した方法

componentsフォルダにPrivateRoute.jsファイルを作成してください。

PrivateRouteの中でuserオブジェクトにアクセスを行い、userオブジェクトがnull(ログインしていない場合)には/loginにリダイレクトされ、ログインしている場合はchildrenの内容が表示されます。


import { Navigate } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';

const PrivateRoute = ({ children }) => {
  const { user } = useAuthContext();
  if (!user) {
    return <Navigate to="/login" />;
  }
  return children;
};

export default PrivateRoute;

PrivateRouteコンポーネントが作成できたらアクセス制限したいコンポーネントをPrivateRouteコンポーネントでラップします。


import Home from './components/Home';
import SignUp from './components/SignUp';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Routes>
            <Route
              path="/"
              element={
                <PrivateRoute>
                  <Home />
                </PrivateRoute>
              }
            />
            <Route path="/signup" element={<SignUp />} />
            <Route path="/login" element={<Login />} />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

ログインしている場合にはHomeコンポーネントにアクセスすることができますがログインしていない場合にはHomeコンポーネントにアクセスすることはできません。

Home以外にもページを増やした場合には同じようにPrivateRouteコンポーネントでラップすることでアクセス制限を行うことができます。

ログインした場合のログインページへのアクセス

現在の設定ではログインしているかどうかにかかわらずユーザ登録(/signup)、ログインページ(/login)にアクセスすることができます。/(ルート)に対してはPriveateRouteを設定することでアクセス制限をしていましたがユーザ登録、ログインページについてはPublicRouteを設定することでログインしているユーザにはアクセスできないように設定を行います。componentsフォルダにPublicRoute.jsファイルを作成してください。


import { Navigate } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';

const PublicRoute = ({ children }) => {
  const { user } = useAuthContext();
  if (user) {
    return <Navigate to="/" />;
  }
  return children;
};

export default PublicRoute;

App.jsファイルのRouteコンポーネントをPublicRouteに変更します。


import Home from './components/Home';
import SignUp from './components/SignUp';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import PublicRoute from './components/PublicRoute';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Routes>
            <Route
              path="/"
              element={
                <PrivateRoute>
                  <Home />
                </PrivateRoute>
              }
            />
            <Route
              path="/signup"
              element={
                <PublicRoute>
                  <SignUp />
                </PublicRoute>
              }
            />
            <Route
              path="/login"
              element={
                <PublicRoute>
                  <Login />
                </PublicRoute>
              }
            />
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

ここまでの設定が終わるとログインした状態で/signupまたは/loginにアクセスすると/(ルート)にリダイレクトされます。ログインしていない状態からログインすると/loginにリダイレクトされます。

SignUp, LoginコンポーネントのようにPublicRouteを個別の設定ではなくOutletコンポーネントを利用してまとめることができます。

PublicRouteコンポーネントをOutlletコンポーネントを利用してchildrenを書き換えます。


import { Navigate, Outlet } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';

const PublicRoute = ({ children }) => {
  const { user } = useAuthContext();
  if (user) {
    return <Navigate to="/" />;
  }
  return <Outlet />;
};

export default PublicRoute;

Appコンポーネントは下記のように記述することができます。先ほどよりもすっきりしたコードにすることができどのルーティングがPublicRouteなのかもわかりやすくなります。


import Home from './components/Home';
import SignUp from './components/SignUp';
import Login from './components/Login';
import { AuthProvider } from './context/AuthContext';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import PublicRoute from './components/PublicRoute';

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Routes>
            <Route
              path="/"
              element={
                <PrivateRoute>
                  <Home />
                </PrivateRoute>
              }
            />
            <Route element={<PublicRoute />}>
              <Route path="/signup" element={<SignUp />} />
              <Route path="/login" element={<Login />} />
            </Route>
          </Routes>
        </BrowserRouter>
      </div>
    </AuthProvider>
  );
}

export default App;

ログイン後のリダイレクトの設定

ここまでの設定ではログイン画面でログインするとPublicRouteコンポーネントの設定で/(ルート)にリダイレクトされますがsignInWithEmailAndPasswordメソッドの後にリダイレクト処理を追加します。

ログインが正常に行われた場合はuseNavigate Hookを利用して/(ルート)にリダイレクトできるように設定を行います。


import { auth } from '../firebase';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { Link, useNavigate } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;

    signInWithEmailAndPassword(auth, email.value, password.value).then(() => {
      navigate('/');
    });
  //略

ログアウトを行い、ログイン画面からログインを行ってください。正しいメールアドレスとパスワードを入力してログインを行った場合に/(ルート)にリダイレクトされますが何も入れない場合や間違ったメールアドレスまたはパスワードを入れた場合はブラウザ上には何も起こりません。

ブラウザ上では何も変化がありませんがデベロッパーツールのコンソールを見るとエラーが表示されていることが確認できます。

ログイン失敗のエラーメッセージ
ログイン失敗のエラーメッセージ

エラーについてはcatchを利用して取得することができます。errorオブジェクトのcodeとmessageの内容を確認します。


signInWithEmailAndPassword(auth, email.value, password.value)
  .then(() => {
    navigate('/');
  })
  .catch((error) => {
    console.log(error.code);
    console.log(error.message);
  });

codeには”auth/invalid-email”, messageにはFirebase: Error (auth/invalid-email)の文字列が入っていることがわかります。

エラーのCodeとMessage
エラーのCodeとMessage

エラーのCodeについてはFirebaseのドキュメント(https://firebase.google.com/docs/auth/admin/errors)で確認することができます。

Admin Authentication API エラー
Admin Authentication API エラー

ブラウザ上にログイン時のエラーメッセージが表示できるようにエラーのcodeとuseState Hookを利用します。ログイン時に発生するcodeは実際にエラーを発生させることで確認することができます。switch関数を利用してそれぞれのcodeに応じて表示するメッセージを変更しています。


import { auth } from '../firebase';
import { signInWithEmailAndPassword } from 'firebase/auth';
import { Link, useNavigate } from 'react-router-dom';
import { useState } from 'react';

const Login = () => {
  const navigate = useNavigate();
  const [error, setError] = useState('');
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;

    signInWithEmailAndPassword(auth, email.value, password.value)
      .then(() => {
        navigate('/');
      })
      .catch((error) => {
        switch (error.code) {
          case 'auth/invalid-email':
            setError('正しいメールアドレスの形式で入力してください。');
            break;
          case 'auth/user-not-found':
            setError('メールアドレスかパスワードに誤りがあります。');
            break;
          case 'auth/wrong-password':
            setError('メールアドレスかパスワードに誤りがあります。');
            break;
          default:
            setError('メールアドレスかパスワードに誤りがあります。');
            break;
        }
      });
  };

  return (
    <div>
      <h1>ログイン</h1>

      <form onSubmit={handleSubmit}>
        {error && <p style={{ color: 'red' }}>{error}</p>}
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            type="password"
            placeholder="password"
          />
        </div>
        <div>
          <button>ログイン</button>
        </div>
        <div>
          ユーザ登録は<Link to={'/signup'}>こちら</Link>から
        </div>
      </form>
    </div>
  );
};

export default Login;

何もメールアドレスに入力しない場合は以下のようにエラーメッセージが表示されるようになります。

ログイン失敗時のエラー表示
ログイン失敗時のエラー表示

サインアップ時のリダイレクト設定

ユーザ登録(サインアップ)を行った後も/(ルート)にリダイレクトできるように設定を行います。エラーメッセージの表示もログイン画面と同様に行っておきます。サインアップ時のエラーcodeにはログイン時とは異なりweak-passwordやemail-already-in-useなでがあります。


import { auth } from '../firebase';
import { createUserWithEmailAndPassword } from 'firebase/auth';
import { useNavigate, Link } from 'react-router-dom';
import { useState } from 'react';

const SignUp = () => {
  const navigate = useNavigate();
  const [error, setError] = useState('');
  const handleSubmit = (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    createUserWithEmailAndPassword(auth, email.value, password.value)
      .then(() => {
        navigate('/');
      })
      .catch((error) => {
        console.log(error.code);
        switch (error.code) {
          case 'auth/invalid-email':
            setError('正しいメールアドレスの形式で入力してください。');
            break;
            case 'auth/weak-password':
              setError('パスワードは6文字以上を設定する必要があります。');
              break;
          case 'auth/email-already-in-use':
            setError('そのメールアドレスは登録済みです。');
            break;
          default:
            setError('メールアドレスかパスワードに誤りがあります。');
            break;
        }
      });
  };

  return (
    <div>
      <h1>ユーザ登録</h1>
      <form onSubmit={handleSubmit}>
        {error && <p style={{ color: 'red' }}>{error}</p>}
        <div>
          <label htmlFor="email">メールアドレス</label>
          <input id="email" name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label htmlFor="password">パスワード</label>
          <input
            id="password"
            name="password"
            type="password"
            placeholder="password"
          />
        </div>
        <div>
          <button>登録</button>
        </div>
        <div>
          ユーザ登録済の場合は<Link to={'/login'}>こちら</Link>から
        </div>
      </form>
    </div>
  );
};

export default SignUp;
ここではFirebaseのサーバ側でバリデーションのみ行っていますが通常はフロントエンド側でのバリデーションも利用します。
fukidashi

動作確認

ログインしていない状態で/(ルート)にアクセスすると/loginにリダイレクトされます。ログインが行われると/(ルート)にリダイレクトされ、/signupや/loginにアクセスすると/(ルート)にリダイレクトされます。/(ルート)のページからログアウトすると/loginにリダイレクトされます。

ここまでの動作確認でReact環境下でのFirebase認証設定の理解が多少でも深まったのではないでしょうか。