React+Redux+TypescriptでFirebaseのLogin機能を実装する

最近Reactをよく触ってて、その際にReactからFirebase Loginを実装する方法について学んだのでその知見を書きます。
Firebase Login機能のサンプルコードは[こちら](https://github.com/anraku/react-firebase-login)に置いてあります。

実装ではなるべくモダンな開発環境で使われてそうなライブラリを使いたかったのでReact、Redux、Typescriptを使っています。
ESLintやPretter、lint-stagedの設定もなるべく追加するようにしています。
また、Reactコンポーネントはクラスを使って実装するのではなく、ここではなるべく関数コンポーネントを使って書くようにしてます。

## アプリの概要
見た目はログイン機能を検証する上で必要最小限にしています。
<img width="304" alt="スクリーンショット 2019-05-11 18.18.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34409/d4128a98-2d5d-cd92-e1af-1b284559f410.png">

Google Loginボタンを押すとGoogleのログイン認証ページにリダイレクトして認証をします。
ユーザ認証が成功するとguestの部分がユーザ名に、ボタンの名前はGoogle Logoutに変わります。

## Presentational ComponentとContainer Component
見た目の部分はcomponents/Auth.tsxで実装しています。
描画する際に必要なプロパティはAuthPropsで定義しています。

```react:components/Auth.tsx
import React, { FC } from 'react';
import firebase from '../config/index';

interface AuthProps {
  loginUser?: firebase.User | null;
  handleLogin?: () => void;
  handleLogout?: () => void;
}

const AuthComponent: FC<AuthProps> = props => {
  const { loginUser, handleLogin, handleLogout } = props;

  return (
    <>
      {loginUser ? (
        <>
          <p>{loginUser.displayName}さんこんにちは</p>
          <button type="button" onClick={handleLogout}>
            Google Logout
          </button>
        </>
      ) : (
        <>
          <p>guestさんこんにちは</p>
          <button type="button" onClick={handleLogin}>
            Google Login
          </button>
        </>
      )}
    </>
  );
};

export default AuthComponent;

```
AuthComponentでは表示に必要な値をprops経由で受け取っている。
ログイン情報などの実体はcontainers/Auth.tsxの方で持っていて、HOCを使用してprops経由でコンポーネントに値を渡しています。
実際に値の渡している部分とHOCを使っている部分は以下の通りです。

```react:containers/Auth.tsx
const AuthContainer: FC<AuthState & DispatchProps> = ({
  loginUser,
  dispatchAuthStatus,
  handleLogin,
  handleLogout,
}) => {
  firebase.auth().onAuthStateChanged(user => {
    dispatchAuthStatus(user);
  });

  return (
    <AuthComponent
      loginUser={loginUser}
      handleLogin={handleLogin}
      handleLogout={handleLogout}
    />
  );
};

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(AuthContainer);
```

HOCを利用することでPresentational ComponentとContainer Componentを分離することができ、上記のコードではAuthComponentをAuthContainerでラップしています。
`export default connect(...`の処理でreduxのstoreと接続したコンポーネントを生成して、これがApp.tsxのrenderで使われます。
コンポーネントに渡すStateとdispachはそれぞれmapStateToProps、mapDispatchToPropsとして定義しています。

```react:containers/Auth.tsx
import { AuthState, changeAuthStatus } from '../reducers/auth';

...

const mapStateToProps = (state: ApplicationState): AuthState => ({
  loginUser: state.auth.loginUser,
});

export interface DispatchProps {
  dispatchAuthStatus: (user: firebase.User | null) => void;
  handleLogout: () => void;
  handleLogin: () => void;
}

const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
  dispatchAuthStatus: (user: firebase.User | null) =>
    dispatch(changeAuthStatus(user)),
  handleLogout: () => {
    firebase.auth().signOut();
    dispatch(changeAuthStatus(null));
  },
  handleLogin: () => {
    const user = firebase.auth().currentUser;
    if (!user) {
      const provider = new firebase.auth.GoogleAuthProvider();
      firebase.auth().signInWithRedirect(provider);
    } else {
      dispatch(changeAuthStatus(firebase.auth().currentUser));
    }
  },
});
```


## Reducerを通した状態遷移
このアプリではユーザのログイン情報をStateとして保持し、StateをReduxを使って管理します。
Reduxを使った状態管理を実現するためにはActionType、ActionCreater、Reducerを実装する必要があります。
実装方法のプラクティスはいくつかあるのですが、このアプリではファイル分割がほとんど必要ないducksパターンを採用します。
Reducerなどを実装したファイルはreducers/配下に配置していて、Reducerのrootとなる部分はreduckers/index.tsで定義しています。

```react:reducers/index.ts
const reducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
  auth,
});

export default reducers;
```
combineReducersは指定したreducerを統合したreducerの状態ツリーを作成します。
作成されたreducerは`index.tsx`内のcreateStoreの引数として渡します。

```react:index.tsx
import reducers from './reducers/index';

...

const store = createStore(reducers, enhancer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'),
);
```

combineReducersにはオブジェクトを渡すだけなので今後reducerが増えたとしても拡張は容易です。今回はreducers/auth.tsから読み込んだreducerのみ追加しています。
auth.tsのreducerではユーザ情報を管理したいのでfirebaseから受け取った認証情報がdispatchされたら新しくstateを更新するようにします。

```react:reducers/auth.ts
// actionType
export const CHANGE_AUTH_STATUS = 'USER/CHANGE_AUTH_STATUS';

// actionCreater
export const changeAuthStatus = (user: firebase.User | null) => ({
  type: CHANGE_AUTH_STATUS as typeof CHANGE_AUTH_STATUS,
  user,
});

// reducer
const auth: Reducer<AuthState, AuthAction> = (
  state: AuthState = initialState,
  action: AuthAction,
) => {
  switch (action.type) {
    case CHANGE_AUTH_STATUS:
      return {
        ...state,
        loginUser: action.user,
      };
    default:
      return state;
  }
};
```

# 補足
## Firebaseの設定
githubには挙げていないですが、Firebaseの設定ファイルはこんな感じにしてます。

```javascript:config/config.js
export const firebaseConfig = {
  apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  authDomain: 'sample-project.firebaseapp.com',
  projectId: 'sample-project',
  messagingSenderId: '111111111111',
};
```

# 参考文献
[りあクト！ TypeScriptで始めるつらくないReact開発 第2版](
https://oukayuka.booth.pm/items/1312652)
[りあクト！ TypeScriptで極める現場のReact開発](https://oukayuka.booth.pm/items/1312815)
https://medium.freecodecamp.org/how-to-build-a-chat-application-using-react-redux-redux-saga-and-web-sockets-47423e4bc21a
https://medium.com/@resir014/a-type-safe-approach-to-redux-stores-in-typescript-6474e012b81e
[JavaScript による Google ログインを使用した認証](https://firebase.google.com/docs/auth/web/google-signin?hl=ja)