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

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

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

![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/73ff039a-fa8c-5f44-0234-744f209eec6d.png)

## 2021.04.17:追記
全体を見直しコードを若干変更しました。


## ファイル構造

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

```console
- 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もインストールします。

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


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

```js
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` を実装します。

```react:/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に適応します。
全体でユーザ情報の変更を取得することができるようになります。

```react: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を設定しています。

```react:/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から取得して表示しています。


```react: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に反映、していない場合はサインインページに飛ばすようにします。

```react:/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コンポーネントで囲みます。

```react: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;

```

これで終了です。
実行すると以下のようにサインインページが表示されます。
<img width="467" alt="スクリーンショット 2020-05-17 12.13.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/97d444fb-dee8-8d1d-2998-47f96839fe14.png">

サインイン後
<img width="446" alt="スクリーンショット 2020-05-17 12.21.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252067/608fbe8c-6a8f-1cce-6ebb-ba8e0e4e9070.png">


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



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