LoginSignup
8
6

More than 3 years have passed since last update.

firebase auth + react hooks (context, reducer) で認証

Last updated at Posted at 2020-05-17

概要

Firebase の認証機構を使って、Reactでサインイン周りを実装するサンプル(メモ)です。

実装イメージはこんな感じです。

図1.png

2021.04.17:追記

全体を見直しコードを若干変更しました。

ファイル構造

色々ファイルを追加するので予めファイル構造を。

- src
    - components
        - AuthCheck.js
    - cnofig
        - Firebase.js
    - store
        - userStore.js
    - App.js
    - Dashboard.js
    - index.js
    - Signin.js
    - App.css

準備

create-react-appで雛形を用意しておいてください。
firebase、 react-router-dom、そのほかにloading中に他コンポーネントを触らせないようにする react-loading-overlayもインストールします。

# npm i -S firebase react-router-dom react-loading-overlay

firebase設定の詳細は割愛します。
configの下にFirebase.jsを作成し、利用するfirebaseの構成オブジェクトを以下のように保存しておいてください。

import firebase from 'firebase';

const firebaseConfig = {
  apiKey: "api-key",
  authDomain: "project-id.firebaseapp.com",
  databaseURL: "https://project-id.firebaseio.com",
  projectId: "project-id",
  storageBucket: "project-id.appspot.com",
  messagingSenderId: "sender-id",
  appID: "app-id",
};


firebase.initializeApp(firebaseConfig);

export default firebase;

ユーザ情報格納部分の実装

ユーザ情報は、propsの代わりにuseContextというhookを使って保存するようにします。contextを使うことで子コンポーネントにpropsを引き継いでいく必要がないという利点があります。ユーザ情報の変更はuseReducerを利用します。

この2つを使って userStore.js を実装します。

/src/store/userStore.js
import React, { createContext, useReducer } from "react";

const initialState = { user: null }

const UserContext = createContext();

const StateProvider = ({ children }) => {
    const [state, dispatch] = useReducer((state, action) => {
      case "login":
        if (action.payload) {
          return { user: action.payload.user };
        }
        return state;
      case "logout":
        return initialState;
      default:
        return state;
    }}, initialState);
    return <UserContext.Provider value={{ state, dispatch }}>{children}</UserContext.Provider>
}

export { UserContext, StateProvider }

UserContextにユーザ情報を保持します。 StateProviderは、子コンポーネントにコンテキストの変更を提供するためのコンポーネントで、useReducerを利用して状態管理を来ないます。stateがuseReducerによって管理される状態で、dispatchは状態変更を行うための関数です。

コンテキストを全体で共有できるようにする

次に作成したStateProviderをindex.jsに適応します。
全体でユーザ情報の変更を取得することができるようになります。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { StateProvider } from './store/userStore'

const app = (
        <StateProvider>
            <App />
        </StateProvider>
)
ReactDOM.render(app, document.getElementById('root'));

Signinとtopページ(サインイン後)ページを作る

先にサインインページを作ります。
handleSigninでfirebaseにメールアドレスとパスワードでサインインします。(firebaseのログインプロバイダ メール/パスワード を有効にしておく必要があります)
認証に成功した場合は、dispatchでユーザ情報を設定し、トップページに移動させ、失敗した場合はアラートを表示させるようにしています。
ユーザ情報としてサインインを実行した時に戻ってきたfirebase.Userを設定しています。

/Signin.js
import React, { useState, useContext } from "react";
import { UserContext } from "./store/userStore";
import { useHistory } from "react-router-dom";
import firebase from "./config/Firebase";

const Signin = () => {
    const { state, dispatch } = useContext(UserContext);
    const history = useHistory();
    const [formdata, setFormdata] = useState({
        email: "",
        password: ""
    });

    const handleSignin = async () => {
        try {
            const auth = await firebase.auth().signInWithEmailAndPassword(formdata.email, formdata.password);

            if (user) {
                await dispatch({type: "login", payload: {user: auth.user }});
                history.push("/");
            } else {
                alert("エラー!");
            }

        } catch (e) {
            alert("認証失敗");
        }
    }

    const handleChange = (e) => {
        e.preventDefault();
        setFormdata({ ...formdata, [e.target.name]: e.target.value });
    }

    return (
        <div>
            <h2>サインインページ</h2>
            <div>
                email: <input type="text" id="email" name="email" onChange={handleChange} value={formdata.email} />
            </div>
            <div>
                password: <input type="password" id="password" name="password" onChange={handleChange} value={formdata.password} />
            </div>
            <div>
                <button onClick={handleSignin}>サインイン</button><br />
            </div>
        </div>
    )
}

export default Signin;

次に、トップページを実装します。
トップページでは、サインイン時に取得したユーザ情報(state.user.email)をcontextから取得して表示しています。

Dashboard.js
import React, { useContext } from "react";
import { UserContext } from "./store/userStore";
import firebase from "./config/Firebase";
import { useHistory } from "react-router-dom"

const Dashboard = () => {
    const { state, dispatch } = useContext(UserContext);
    const history = useHistory();

    const handleSignout = async () => {
        await firebase.auth().signOut();
        dispatch({type: "logout", payload: null});
    }

    return (
        <div>
            <h2>ダッシュボード</h2>
            {console.log(state.user)}
            <p>こんにちは {state.user ? state.user.email : null} さん</p>
            <hr />
            <button onClick={handleSignout}>サインアウト</button>
        </div>
    )
}

export default Dashboard;

認証チェックAuthCheckの実装

firebaseにすでにサインインしているかどうかをチェックし、している場合はユーザ情報をcontextに反映、していない場合はサインインページに飛ばすようにします。

/src/components/AuthCheck.js
import React, { useContext, useState, useEffect } from "react";
import { UserContext } from '../store/userStore';
import { Redirect } from "react-router-dom";
import firebase from "../config/Firebase";
import LoadingOverlay from 'react-loading-overlay';

const AuthCheck = ({ children }) => {

    const { state, dispatch } = useContext(UserContext);
    const [checked, setChecked] = useState(false);

    useEffect(() => {
        const check = async () => {
            firebase.auth().onAuthStateChanged(async user => {
                console.log(user);
                if (user) {
                    dispatch({type: "login", payload: { user: user }});
                }
                setChecked(true);
            });
        }
        check();
    }, [])

    if (checked) {
        if (state.user) {
            return children;
        }
        else {
            return <Redirect to="/signin" />
        }
    } else {
        return (
            <LoadingOverlay
                active={true}
                spinner
                text='Loading...'
            >
                <div style={{ height: '100vh', width: '100vw' }}></div>
            </LoadingOverlay>
        );
    }
}

export default AuthCheck;

ルーティングの設定

最後にApp.jsに各ページのルーティングを設定します。

Dashboardはサインイン後に表示させるので、Authコンポーネントで囲みます。

App.js
import React from 'react';
import './App.css';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";

import AuthCheck from './components/AuthCheck';
import Signin from './Signin';
import Dashboard from './Dashboard';

function App() {
    return (
        <div className="App">
            <Router>
                <Switch>
                    <Route path="/signin" name="サインイン" exact component={Signin} />
                    <AuthCheck>
                        <Route path="/" name="ダッシュボード" exact component={Dashboard} />
                    </AuthCheck>
                </Switch>
            </Router>
        </div>
    );
}

export default App;

これで終了です。
実行すると以下のようにサインインページが表示されます。
スクリーンショット 2020-05-17 12.13.27.png

サインイン後
スクリーンショット 2020-05-17 12.21.57.png

サインアウトするとサインインページに戻ります。

また、firebaseのサインイン情報はブラウザにキャッシュされているので、サインアウトせずにアプリを終了させ、再度アプリを起動するとサインインしたままなので、トップページ(Dashboard.js)に遷移することが確認できます。

8
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
6