ReactのHooksとContextを使ってFirebaseのユーザーログイン状態を管理する

#はじめに
ReactのHooksの使い方メモです。
FirebaseのAuthenticationを使って得たユーザーのログイン状態を、HooksとContextを使って管理します。(Reduxは使用しません）

#ねらい
1. ネストの異なる複数のコンポーネントからユーザー情報を参照する
2. 更新されたユーザー情報を、関連するコンポーネント全体に反映する

ここでは次のようなネスト構造を持つSPAをターゲットとします。

```
App
 |-MainPage
    |-Header
    |-Content
 ...
```
#前提知識
次の内容については概要をおさえていることを前提としています。

Firebase Authentication:
https://firebase.google.com/docs/auth?hl=ja
React Hooks:
https://reactjs.org/docs/hooks-intro.html
React Context:
https://reactjs.org/docs/context.html

#使用技術
* React
  * Context
  * Hooks
     * useReducer
     * useEffect
* Firebase
  * Authentication

※今回、基本のHookであるuseStateは使用しません。
#進め方

##最初のステップ
ユーザー情報は最上位のAppからContextを使って下位コンポーネントに配信します。

```jsx:App.jsx
import React from 'react';
import MainPage from './MainPage';
import AuthContext from '../AuthContext';

function App() {
// valueの与え方については後述
  return (
      <AuthContext.Provider value={後述}>
        <MainPage />
      </AuthContext.Provider>
  );
}

export default App;
```
```js:AuthContext.js
import React from 'react';
const AuthContext = React.createContext({});
export default AuthContext;
````
このように記述することで、下位のコンポーネントから次のようにアクセスができるようになります（createContextの第一引数はuserの初期値となります）。


```jsx:Content.jsx
import React from 'react';
import AuthContext from '../AuthContext';

export default function () {
    return (
        <AuthContext.Consumer>
            {
                user => {
                    // userを使った処理
                }
            }
        </AuthContext.Consumer>
    )
}

```
Contextによって、userに変更が発生すると全てのConsumerは再レンダリングされます。

##ログイン状態の管理
AuthContext.Providerに与えるユーザー情報をログイン状態に応じて変更する必要があります。この状態の変更を、[useReducer](https://reactjs.org/docs/hooks-reference.html#usereducer)という特殊Hookを使って解決します。

```diff
+ import React, { useReducer }  from 'react';
+ import authReducer from '../authReducer';
  import MainPage from './MainPage';
  import AuthContext from '../AuthContext';

  function App() {
+   const [state, dispatch] = useReducer(authReducer.reducer,authReducer.initialState);
    return (
+     <AuthContext.Provider value={state}>
        <MainPage />
      </AuthContext.Provider>
    );
  }

```
```js:authReducer.js
const initialState = {};

const reducer = (state,action) =>{
  switch (action.type) {
    case 'login':
      return action.payload.user;
    case 'logout':
      return initialState;
    default:
      return state;
  }
};

export default {
    initialState,
    reducer
};
```

##ログイン状態の更新検知
ユーザーのログイン状態の更新検知はFirebaseの[オブザーバー](https://firebase.google.com/docs/auth/web/manage-users?hl=ja)を使います。ログインの状態変更時にdisptatchを呼び出したいために、次のようなリスナー登録関数を用意します。

```js:auth.js
export const listenAuthState = (dispatch) => {
    return firebase.auth().onAuthStateChanged(function (user) {
        if (user) {
            // User is signed in.
            dispatch({
                type: 'login',
                payload: {
                    user
                }
            });
        } else {
            // User is signed out.
            // ...
            dispatch({
                type: 'logout',
            });
        }
    });
}

export const login = () => {
    var provider = new firebase.auth.GoogleAuthProvider();
    firebase.auth().signInWithPopup(provider).catch(function (error) {
        console.error(error);
    });
}
```
これにて、AppでlistenAuthStateを呼び出せば良いわけですが、呼び出しが毎回のレンダリングで行われないように注意しなければいけません。
一度だけリスナーを登録するために、[useEffect](https://reactjs.org/docs/hooks-reference.html#useeffect)というHookを使います。

```diff
+ import React, { useReducer, useEffect }  from 'react';
+ import { listenAuthState } from '../auth';
  import authReducer from '../authReducer';
  import MainPage from './MainPage';
  import AuthContext from '../AuthContext';

  function App() {
    const [state, dispatch] = useReducer(authReducer.reducer,authReducer.initialState);
+   useEffect(()=>{
+       return listenAuthState(dispatch);
+   },[]);
    return (
        <AuthContext.Provider value={state}>
          <MainPage />
        </AuthContext.Provider>
    );
  }

```
このuseEffectはコンポーネントのマウントおよび更新時に実行されます。
useEffectの第二引数に空配列を指定することで、マウント時に一度だけ実行されることになります（クラスコンポーネントのcomponentDidMountと同じ）
最後に、Headerコンポーネントでログインを呼び出します。ユーザーの状態が変われば、各Consumerが再レンダリングされ、ユーザーの状態に応じた表示が可能になります。

```jsx:Header.jsx
import React from 'react';
import AuthContext from '../AuthContext';
import { login, logout } from '../auth';


export default function () {
    return (
        <div>
            <AuthContext.Consumer>
                {user =>
                    Object.keys(user).length === 0 ? (
                        <button onClick={() => { login(); }} >LOGIN</button>
                    ) : (
                            <button onClick={() => { logout(); }} >LOG OUT</button>
                        )}
            </AuthContext.Consumer>
        </div>
    )
}
```

#最後に
[こちら](https://github.com/mktu/react-auth-sample)にサンプルコードを公開しています