【React+Firebase Auth】ログイン周りのクラス設計を考える

ReactとFirebase Authenticationのアプリでログイン周りの最適なクラス設計を考えたい。

Global Stateの管理は[`unstated`](https://github.com/jamiebuilds/unstated)を使っていましたが、同じ作者がReact Hooksを用いた[`unstated-next`](https://github.com/jamiebuilds/unstated-next)を開発されていたのでそちらに乗り換えます。

## 前提

Windows10で作成します。
アプリの土台は`create-react-app`で作成しています。

Twitterログインだけを実装します。Twitterアプリの登録は済ませてあります。


## とりあえず動くものを

`npx create-react-app`を実行した後、必要なパッケージを導入します。

```
npm install unstated-next
npm install firebase
```

まずは馬鹿正直に一つのtsxファイルにぶち込む💪

```tsx:src/App.tsx
import React from 'react';
import './App.css';
import { createContainer } from "unstated-next";
import firebase from "firebase/app";
import "firebase/auth";

firebase.initializeApp({
  apiKey: "this-is-your-api-key",
  authDomain: "your-app-name.firebaseapp.com",
  databaseURL: "https://your-app-name.firebaseio.com",
  projectId: "your-app-name",
  storageBucket: "your-app-name.appspot.com",
  messagingSenderId: "999999999999",
  appId: "9:999999999999:web:999xxx999xxx999xxx999x",
  measurementId: "Z-ZZZZZZZZ999"
});

function useUserState() {
  const [user, setUser] = React.useState<firebase.User | null>(null);

  firebase.auth().onAuthStateChanged(user => setUser(user));

  const signIn = async () => {
    try {
      const provider = new firebase.auth.TwitterAuthProvider();
      await firebase.auth().signInWithPopup(provider);
    } catch (error) {
      setUser(null);
    }
  }

  const signOut = async () => {
    await firebase.auth().signOut();
  }

  return { user, signIn, signOut }
}

const userContainer = createContainer(useUserState);

const LoginPage = () => {
  const userState = userContainer.useContainer();
  return (
    <div className="App">
      {userState.user
        ?
        <div className="isSignedIn">
          <div className="rowContent">
            <button
              className="signInButton"
              onClick={userState.signOut}>
              サインアウト
          </button>
          </div>
          <div className="rowContent">
            <img src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} />
          </div>
          <div className="rowContent">
            <p>{userState.user.displayName}</p>
          </div>
          <div className="rowContent">
            <p>{userState.user.uid}</p>
          </div>
        </div>
        :
        <div className="rowContent">
          <button
            className="signInButton"
            onClick={userState.signIn}>
            サインイン
        </button>
        </div>
      }
    </div>
  );
}

const App: React.FC = () => {
  return (
    <userContainer.Provider>
      <LoginPage />
    </userContainer.Provider>
  );
}

export default App;
```

#### 動作確認
![login-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299625/a81ede78-a904-0245-29aa-61b18d6f644c.gif)


Twitterのアカウントでログインして画像と名前を表示するだけのアプリケーションができました。
このUIはそのままに、読みやすいソースコードに分割できないか考えます。

クラス設計のベストエフォートがわからないので本当に自己流です...。ご容赦！

## Firebaseの初期化処理

`firebase.initializeApp()`は最初に必ず通過してほしく、かつプロジェクトが変わってもAPIKeyを書き換えるだけで済むようにファイルひとつにしておきます。

```ts:src/firebase.ts
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";

firebase.initializeApp({
  apiKey: "this-is-your-api-key",
  authDomain: "your-app-name.firebaseapp.com",
  databaseURL: "https://your-app-name.firebaseio.com",
  projectId: "your-app-name",
  storageBucket: "your-app-name.appspot.com",
  messagingSenderId: "999999999999",
  appId: "9:999999999999:web:999xxx999xxx999xxx999x",
  measurementId: "Z-ZZZZZZZZ999"
});

export default firebase;
```

このソースが`export`する`firebase`を参照すれば`initializeApp()`済みであることが保証されます。
`import`は利用するFirebaseのサービスに合わせて追加削除します。

ちなみに`initializeApp()`に渡しているAPIKeyは第三者に知られても問題ないようです。GitHubのパブリックなリポジトリにこのままプッシュできて楽ですね。

## ログイン処理

Firebase Authenticationでログインする処理を記述するクラスを作成します。

```ts:src/service/AuthService.ts
import firebase from "../firebase";

export class AuthService {
	onAuthStateChanged = (observer: (user: firebase.User | null) => void) => {
		firebase.auth().onAuthStateChanged(observer);
	}

	signInWithTwitter = async () => {
		const provider = new firebase.auth.TwitterAuthProvider();
		await firebase.auth().signInWithPopup(provider);
	}

	signOut = async () => {
		await firebase.auth().signOut();
	}
}
```

必要に応じて`signInWithGoogle`などを追加します。エラー処理もしてあるとベストです(手抜き)。

## Container

`unstated-next`を用いてstate管理するContainerを作成します。

```ts:src/container/UserContainer.ts
import React from 'react';
import { createContainer } from "unstated-next";
import firebase from "../firebase";
import { AuthService } from '../service/AuthService';

const useUserState = () => {
  const authService = new AuthService();
  const [user, setUser] = React.useState<firebase.User | null>(null);

  authService.onAuthStateChanged(user => setUser(user));

  const signIn = async () => { await authService.signInWithTwitter(); }

  const signOut = async () => { await authService.signOut(); }

  return { user, signIn, signOut }
}

const userContainer = createContainer(useUserState);

export default userContainer;
```

内部でログイン処理を委譲する`AuthService`のインスタンスを生成しています。
本当は中で`new`したくないのですがいい方法が思いつきませんでした。どなたか知見があれば教えていただけると助かります。

## ログインページ

Containerを参照して管理している状態を画面に表示するコンポーネントを作成します。

```tsx:src/components/LoginPage.tsx
import React from 'react';
import './App.css';
import userContainer from "../container/UserContainer";

const LoginPage = () => {
	const userState = userContainer.useContainer();

	return (
		<div className="App">
			{userState.user
				?
				<div className="isSignedIn">
					<div className="rowContent">
						<button
							className="signInButton"
							onClick={userState.signOut}>
							サインアウト
						</button>
					</div>
					<div className="rowContent">
						<img alt="icon"
							src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} />
					</div>
					<div className="rowContent">
						<p>{userState.user.displayName}</p>
					</div>
					<div className="rowContent">
						<p>{userState.user.uid}</p>
					</div>
				</div>
				:
				<div className="rowContent">
					<button
						className="signInButton"
						onClick={userState.signIn}>
						サインイン
					</button>
				</div>
			}
		</div>
	);
}

export default LoginPage;
```

UIとロジックが分離されていて読みやすくなっているかと思います。

## ルート

`LoginPage.tsx`を`Provider`で囲うルートのコンポーネントです。

```tsx:src/App.tsx
import React from 'react';
import LoginPage from "./components/LoginPage";
import userContainer from "./container/UserContainer";

const App: React.FC = () => {
  return (
    <userContainer.Provider>
      <LoginPage />
    </userContainer.Provider>
  );
}

export default App;
```

`App`をレンダリングすれば上の方に載せたgifと同じものがブラウザに表示されます。

## ディレクトリ構成

`src`配下のディレクトリ構成は下記の通りになっています。

```
│  App.tsx
│  firebase.ts
│
├─components
│      LoginPage.css
│      LoginPage.tsx
│
├─container
│      UserContainer.ts
│
└─service
        AuthService.ts
```

ログイン周りに限らず、ロジックを記述するソースは`service`に、状態管理に関するソースは`container`に保存します。
さらにFirestoreにアクセスするアプリの場合は`repository`フォルダを作成して`UserRepository`クラスなんかを作ります。

## まとめ

ソースの再利用がしやすいコードを意識してみましたがいかがでしょうか。
優しくマサカリぶん投げてくれる方がいらっしゃればご指摘お願いいたします。