本文書ではReact環境でのFirebaseを使った認証について説明を行っています。認証の処理を実装するためにはReact RouterをはじめReact HookのuseState, useContext, useEffectなどReactの基礎知識が必要となります。

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

Firebase上でのプロジェクトの作成や認証の設定、メソッドはReactのみの固有の設定ではなくVue.jsを含め他のフレームワークでも利用できます。

本文書ではReactの下記の機能を利用しているのでFirebase認証設定を理解するのと同時にReactの使い方も同時に学ぶことができます。

  • React Router
  • useEffect
  • useState
  • useContext
  • useRef

動作確認はmacOS上で行っています。

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の機能を利用するためには最初にプロジェクトの作成が必要となります。画面中央にある”プロジェクトを作成”ボタンをクリックしてください。

Firebase Welcomページ
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="AIzaSyC35QTjhdfuMlHHYrfxEBfRjwe4MmCjA8U"
REACT_APP_FIREBASE_AUTH_DOMAIN="react-auth-74444.firebaseapp.com"
REACT_APP_FIREBASE_PROJECT_ID="react-auth-74444"
REACT_APP_FIREBASE_STORAGE_BUCKET="react-auth-74444.appspot.com"
REACT_APP_FIREBASE_MESSAGE_SENDER_ID="465176947491"
REACT_APP_FIREBASE_APP_ID="1:465176947490:web:01626bd25608anad3537b"

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


import firebase from 'firebase/app';
import '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,
};

firebase.initializeApp(firebaseConfig);

export const auth = firebase.auth();

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

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


 % cd react-firebase-auth
 % npm install --save firebase

Firebaseの認証の設定

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

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

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

Authenticaionページ
Authenticationページ

ユーザがサインイン(ログイン)するために利用することができるログインプロバイダーの一覧が表示されます。Googleアカウントなども利用することができますがここでは一番上のメール/パスワードを利用します。一般的なサービスではユーザ登録としてメールアドレスを登録してもらうことが多いかと思います。

ステータス列を見るとデフォルトではすべて無効になっています。有効にするために右側に表示されている”鉛筆”のアイコンをクリックしてください。

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

メールリンクを利用してログインする方法も有効にすることができますがメールアドレスとパスワードを使用して登録を有効にしてください。有効にしたら”保存”ボタンをクリックしてください。

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

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

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

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

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

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

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

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

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


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

export default SignUp;

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>メールアドレス</label>
          <input name="email" type="email" placeholder="email" />
        </div>
        <div>
          <label>パスワード</label>
          <input 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メソッドの引数には入力フォームから取得したメールアドレスとパスワードを指定します。


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

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

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

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

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

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

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

ユーザ情報の共有(Context)

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

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メソッドの書式は下記の通りです。


firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    // User is signed in.
  }
});

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


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

const AuthContext = createContext();

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

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

  const value = {
    user,
  };

  useEffect(() => {
    const unsubscribed = auth.onAuthStateChanged((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 { useAuthContext } from '../context/AuthContext';

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

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

export default SignUp;

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

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

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

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

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

React Routerの設定

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

React Routerの基礎については下記で公開しているので参考にしてください。本文書はv5を利用していますがv6を利用したい場合は2つ目の記事を参考にしてください。

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


 % npm install react-router-dom

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


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

function App() {
  return (
    <AuthProvider>
      <div style={{ margin: '2em' }}>
        <BrowserRouter>
          <Route path="/signup" component={SignUp} />
        </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に追加したコンポーネントのルーティングを追加します。”/”にはexactを設定しますが”/”を設定しないと/signupにアクセスした場合もURLに”/”を含むのでHomeコンポーネントの内容が一緒に表示されます。


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

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

export default App;

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

ログインページの作成

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


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

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

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

export default Login;
ログインページ
ログインページ

Homeページの作成

Homeページは後ほどログインができたユーザのみアクセスができるように設定を行いますが、ホームページでログアウトができるように設定を行っておきます。firabase.jsからauthをimportしています。サインアウトはauth.signOutメソッドで行うことができます。ログアウト後にloginページに移動できるようにReact RouterのuseHistory Hookを使います。useHistoryから作成するhistoryのpushメソッドにURLを指定するとその場所に移動することができます。


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

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

export default Home;

設定後ログアウトボタンを押すとloginページにリダイレクトされることを確認してください。ログイン画面からログインを行うことが可能ですがログイン後の処理は何も設定されていないのでログインができてもブラウザには変化はありません。

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

アクセスの制限設定

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

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

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

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

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


import { auth } from '../firebase';
import { useHistory, Redirect } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';
const Home = () => {
  const history = useHistory();
  const { user } = useAuthContext();
  const handleLogout = () => {
    auth.signOut();
    history.push('/login');
  };

  if (!user) {
    return <Redirect 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になるまで表示させないように設定を行います。


//略

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

  const value = {
    user,
    loading,
  };

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

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

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

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

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

ページが開くまで少し時間がかかるのでロード中であることを表示させたい場合は以下のように設定することができます。一瞬ですが、画面には”loading…”が表示されます。


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

※次にPrivateRouteを利用した方法を説明します。PrivateRouteを設定する前にはHome.jsファイルで設定した分岐に関数するコードは削除しておいてください。


import { auth } from '../firebase';
import { useHistory } from 'react-router-dom';
const Home = () => {
  const history = useHistory();
  const handleLogout = () => {
    auth.signOut();
    history.push('/login');
  };

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

export default Home;

PrivateRouteを利用した方法

Homeコンポーネントによる分岐の方法に比べて少し複雑になりますが、PrivateRouteを作成することでアクセス制限を行うことができます。Routerコンポーネントをラップする形で作成を行います(RouterコンポーネントをPrivateRouteで包むことでアクセス制限を行う)。componentsフォルダにPrivateRoute.jsファイルを作成してください。

PrivateRouteコンポーネント内でも実行しているはuserオブジェクトを使った分岐です。


import { Route, Redirect } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';
const PrivateRoute = ({ component: Component, ...rest }) => {
  const { user } = useAuthContext();
  return (
    <Route
      {...rest}
      render={(routeProps) => {
        return user ? <Component {...routeProps} /> : <Redirect to="/login" />;
      }}
    />
  );
};

export default PrivateRoute;

…restにはPrivateRouteに設定されているpropsのpathとexactの値が入っています。Component(大文字のC)には/signにアクセスすればSign, /loginであればLoginコンポーネントになります。またroutePropsにはroute propsであるmatch, location, historyが含まれています。

App.jsファイルでRouteコンポーネントを作成したPrivateRouteに変更します。


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

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

export default App;

ログインした状態であればホームページにアクセスでき、ログインしていない場合は/loginにリダイレクトされることを確認してください。

上記のPrivateRouteコンポーネントのコードを初めてみた人は何をしているのかわからないと思うのでコードを少し変更して動作を確認します。変更したコードではrender関数を利用せずに同様の処理を行おうとしていますがuserオブジェクトがない場合に/loginと/signupにアクセスするとRedirectにはexactの設定がないので必ずRedirectが実行されることになります。そのため/signupでアクセスすると/loginにリダイレクトされます。


const PrivateRoute = ({ component: Component, ...rest }) => {
  const { user } = useAuthContext();
  return user ? (
    <Route {...rest} component={Component} />
  ) : (
    <Redirect to="/login" />
  );
};

render関数を利用している場合はRouteコンポーネントでexactがチェックされた後にuserの分岐が行われるため/signupにアクセスした場合にuserによる分岐処理が行われることはありません。render関数の場合もRouteコンポーネントに設定されている{…rest}を削除するとexactのチェックがないため/signupにアクセスすると/loginにリダイレクトされます。

上記のコードでもcomponentや…restに入っている値が気になる場合は下記のようにコードを書き換えることができます。これを見ればPrivateRouteコンポーネントの最初の変更コードの理解も少しは進むかと思います。


const PrivateRoute = ({ component, exact, path }) => {
  const { user } = useAuthContext();
  return user ? (
    <Route exact={exact} path={path} component={component} />
  ) : (
    <Redirect to="/login" />
  );
};

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

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


import { Route, Redirect } from 'react-router-dom';
import { useAuthContext } from '../context/AuthContext';
const PublicRoute = ({ component: Component, ...rest }) => {
  const { user } = useAuthContext();
  return (
    <Route
      {...rest}
      render={(routeProps) => {
        return !user ? <Component {...routeProps} /> : <Redirect to="/" />;
      }}
    />
  );
};

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 } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import PublicRoute from './components/PublicRoute';

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

export default App;

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

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

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

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


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

const Login = () => {
  const history = useHistory();
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    await auth.signInWithEmailAndPassword(email.value, password.value);
    history.push('/');
  };
  //略

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

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

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

The email address is badly formatttedというエラーが表示されているのでブラウザ上にエラーメッセージが表示されるようにtry, catchとuseStateを使って設定を行います。erorrに値がある場合のみブラウザ上に赤文字で表示されるように設定を行なっています。


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

const Login = () => {
  const history = useHistory();
  const [error, setError] = useState('');
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    try {
      await auth.signInWithEmailAndPassword(email.value, password.value);
      history.push('/');
    } catch (error) {
      console.log(error);
      setError(error.message);
    }
  };

  return (
    <div>
      <h1>ログイン</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <form onSubmit={handleSubmit}>
  //略

ログインしている場合はログアウトを行い、再度誤った情報でログインを行ってください。ブラウザ上にエラーが表示されます。

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

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

ユーザ登録(サインアップ)を行った後も/(ルート)にリダイレクトできるように設定を行います。エラーメッセージの表示もログイン画面と同様に行っておきます。


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

const SignUp = () => {
  const history = useHistory();
  const [error, setError] = useState('');
  const handleSubmit = async (event) => {
    event.preventDefault();
    const { email, password } = event.target.elements;
    try {
      await auth.createUserWithEmailAndPassword(email.value, password.value);
      history.push('/');
    } catch (error) {
      setError(error.message);
    }
  };

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

export default SignUp;

動作確認

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

これでReact環境下でのFirebase認証設定の理解が深まったのではないでしょうか。

Googleアカウントを利用した認証

Firebaseの認証ではGoogleアカウントも利用することができます。Googleアカウントを利用した場合の動作確認も行っておきましょう。Googleの行にカーソルを合わせると鉛筆のマークが表示されるのでそのままクリックしてください。

Googleを選択
Googleを選択

有効にするを選択し、プロジェクトのサポートメールを選択すると保存が可能となるので、保存ボタンを押してください。

有効にする
有効にする

メールアドレスの時とは異なるメソッドを利用して接続を行うのでfirebase.jsファイルに追加を行います。


import firebase from 'firebase/app';
import '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,
};

firebase.initializeApp(firebaseConfig);

export const provider = new firebase.auth.GoogleAuthProvider(); //追加

export const auth = firebase.auth();

追加したProviderをLogin.jsでimportして利用します。メールアドレスを利用する場合はメールアドレスとパスワードの入力フォームが必要となります。Googleアカウントを利用する場合はログインボタンのみを利用します。


import { auth, provider } from '../firebase';
import { useHistory } from 'react-router-dom';
import { useState } from 'react';

const Login = () => {
  const history = useHistory();
  const [error, setError] = useState('');
  const handleLogin = async (event) => {
    try {
      await auth.signInWithPopup(provider);
      history.push('/');
    } catch (error) {
      console.log(error);
      setError(error.message);
    }
  };

  return (
    <div>
      <h1>ログイン</h1>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <button onClick={handleLogin}>Googleログイン</button>
    </div>
  );
};

export default Login;

ブラウザで確認するとログイン画面にボタンのみ表示されます。

ログインボタンのみ表示
ログインボタンのみ表示

ログインボタンを押すとログイン画面が表示されるのでGoogleアカウントの情報を入力してログインを行なってください。

 ログイン画面表示
ログイン画面表示

ログインが完了するとメールアドレスの時と同様にホームページにリダイレクトされます。/loginページにアクセスしてもホームページにリダイレクトされます。

Googleアカウントでログイン完了
Googleアカウントでログイン完了

Firebaseのユーザの情報を見るとログインを行なってユーザの情報が表示されていることが確認できます。ログインを行ったアカウントを確認することができます。

Firebase上でのログインユーザ情報
Firebase上でのログインユーザ情報

ログアウトの処理は同じなのでホームページからログアウトボタンをクリックするとログアウトすることができます。ログアウトすると”/”にアクセスすることはできません。

Firebase上に作成されたユーザのアカウント情報はログアウトしても消えるものではありません。

Googleアカウントでの認証処理も確認することができました。