【完全版】React 18.2のFirebase v9.10 Authentication(認証)を基礎からマスター
本文書では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プロジェクトの作成を行ってください。
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)”ボタンをクリックしてください。
プロジェクトの名前をつける画面が表示されるので任意の名前をつけてください。ここではreact-authという名前をつけています。
このプロジェクトでGoogleアナリティクスを有効にするがONになっていますが本文書では動作確認なので利用しないためOFFに設定して”プロジェクトを作成”ボタンをクリックしてください。
“プロジェクトを作成”ボタンをクリックしてプロジェクトの作成が完了すると概要ページが表示されます。
ReactからFirebaseのサービスに接続するための認証情報が必要になるのでプロジェクトの概要画面の左から3番目のボタンをクリックしてアプリの登録を行います。
アプリの登録を行うためニックネームの設定を行う必要があります。任意の名前をつけてください。ここではreact-authというニックネームを設定しています。設定したら”アプリ登録”ボタンをクリックしてください。
”アプリの登録”ボタンをクリックするとFirebaseに接続するための情報が表示されます。
表示されている情報はReactで環境変数として利用するため、作成したReactプロジェクトフォルダの直下に.env.localファイルを作成してください。
.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のページが表示されるので”始まる”ボタンをクリックしてください。
ユーザがサインイン(ログイン)するために利用することができるログインプロバイダーの一覧が表示されます。Googleアカウントなども利用することができますがここでは一番左上のネイティブのプロバイダの”メール/パスワード”を利用します。”メール/パスワード”をクリックしてください。
メール/パスワード、メールリンクどちらも無効になっているのでメール/パスワードを”有効にする”に変更して保存ボタンをクリックしてください。
保存後はメール/パスワードのステータスのみ”有効”になっていることが確認できます。
ここまででFirebaseの設定は完了です。
ここからはReact側の設定に入っていきます。
ユーザ登録画面(サインアップ画面)の作成
Firebaseへのユーザ登録が行えるか確認するために最初にユーザ登録画面の作成を行います。
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へのユーザ登録の方法を理解することができました。
ユーザ情報の共有(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>;
}
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などの情報も確認することができます。
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などの情報をここでも確認することができます。この値を削除するとユーザはログイン状態ではなくなります。まだここでは削除しないでください。ログアウト、ログイン機能を実装後に削除して動作確認を行ってみてください。
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にアクセスを行ってください。ユーザ登録画面が表示されればルーティングの設定は正常に行われています。
新たに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を見ても確認することできます。ログアウトした時にキーが削除される等も確認をしてください。
アクセスの制限設定
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についてはFirebaseのドキュメント(https://firebase.google.com/docs/auth/admin/errors)で確認することができます。
ブラウザ上にログイン時のエラーメッセージが表示できるようにエラーの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;
動作確認
ログインしていない状態で/(ルート)にアクセスすると/loginにリダイレクトされます。ログインが行われると/(ルート)にリダイレクトされ、/signupや/loginにアクセスすると/(ルート)にリダイレクトされます。/(ルート)のページからログアウトすると/loginにリダイレクトされます。
ここまでの動作確認でReact環境下でのFirebase認証設定の理解が多少でも深まったのではないでしょうか。