【React】Firebase × Recoil を使った認証機能の実装方法

<!-- ==================================================== -->
# 概要
<!-- ==================================================== -->
**Firebase Authentication**と**Recoil**を使った、ユーザー認証部分の処理を紹介します。
状態管理ライブラリに**Recoil**を使うことで、`redux tool kit`や`useContext`を使うよりもシンプルに実装することができます。

<!-- ==================================================== -->
# 環境
<!-- ==================================================== -->
- windows10
- VSCode
- npm

<!-- ==================================================== -->
# アーキテクチャ
<!-- ==================================================== -->
![アーキテクチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/cc9c1f9a-fdac-dfee-0ceb-bbd566b7ca55.png)

コンポーネントは、3つあります。

- <font color="Red">App.tsx</font>：起点となるコンポーネント
- <font color="Blue">Auth.tsx</font>：認証を行うコンポーネント
- <font color="Green">Main.tsx</font>：ログインしたユーザーが閲覧できるコンポーネント

処理の流れ

① アプリケーションを起動したときにまず<font color="Red">App.tsx</font>が呼ばれます。ログアウト状態から始まるので、<font color="Blue">Auth.tsx</font>へ遷移します。
② Firebase Authに登録しているユーザーを使って、ログインします。
③ Firebase Authのログイン状態を**オブザーバーで監視**します。
④ ③により、ログイン状態が変更されたときに、その状態を**Recoil**に保存します。
⑤ Recoilの状態が変更されたことによって、コンポーネントが再レンダリングされます。
⑥ <font color="Red">App.tsx</font>で再びログイン状態が評価されて、<font color="Green">Main.tsx</font>へ遷移します。

また、<font color="Green">Main.tsx</font>でログアウトした場合、オブザーバー処理によって④が実行されるので、<font color="Blue">Auth.tsx</font>に遷移します。

<!-- ==================================================== -->
# Recoil
<!-- ==================================================== -->
### Recoilとはなんぞや？

https://ics.media/entry/210224/

### Recoilの使い方

https://sbfl.net/blog/2020/05/17/react-experimental-recoil-usage/

### 公式ドキュメント

https://recoiljs.org/

<!-- ==================================================== -->
# プロジェクトの作成
<!-- ==================================================== -->
### Firebase
以下を参考に、プロジェクトを作成してください。

[Firebaseプロジェクトの作成](https://qiita.com/nemutas/items/330cab413bccd9b2e096#firebase%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90)

また、Authenticationの初期設定も行います。このとき、**Mail & Password**を選択します。（お好きな認証方法で大丈夫です）
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/5dfa461d-5140-ecc8-e5e9-2f2dd7b76255.png)

### CRA（Create React Application）
Reactアプリケーションは、CRA（Create React Application）を使って作成します。
プロジェクトフォルダを作成して、そこで以下を実行します。

```:cmd
npx create-react-app . --template typescript
```

プロジェクトに必要なパッケージをインストールします。

```:cmd
npm i firebase recoil
npm i -D @types/recoil
```

<!-- ==================================================== -->
# コーディング
<!-- ==================================================== -->
### Firebase インスタンスの作成

root直下に`.env.local`を追加します。ここに、Firebaseプロジェクトの環境変数を記入します。
CRAでは、環境変数名を`REACT_APP_`で始めることで、環境変数として自動的に認識されるようになります。

```.js:.env.local
REACT_APP_FIREBASE_API_KEY="apiKey"
REACT_APP_FIREBASE_AUTH_DOMAIN="authDomain"
REACT_APP_FIREBASE_DATABASE_URL="https://<projectId>.firebaseio.com"
REACT_APP_FIREBASE_PROJECT_ID="projectId"
REACT_APP_FIREBASE_STORAGE_BUCKET="storageBucket"
REACT_APP_FIREBASE_MESSAGING_SENDER_ID="messagingSenderId"
REACT_APP_FIREBASE_APP_ID="appId"
```

src配下に`firebase.ts`を追加します。

```src/firebase/firebase.ts
import 'firebase/auth';
import firebase from 'firebase/app';

const firebaseConfig = {
	apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
	authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
	databaseURL: process.env.REACT_APP_FIREBASE_DATABASE_URL,
	projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
	storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
	messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
	appId: process.env.REACT_APP_FIREBASE_APP_ID
};

if (!firebase.apps.length) firebase.initializeApp(firebaseConfig);

export const auth = firebase.auth();
```

### Recoil State
ログイン状態を保存しておくための処理を追加します。

```src/store/auth.ts
import { atom } from 'recoil';

export const signInUserState = atom({
	key: 'auth/signIn',
	default: {
		uid: ''
	}
});
```
保持する状態は、ログインユーザーの`uid`です。

### Firebase Auth 処理
Firebase Authに関する処理を追加します。

```src/firebase/authFunctions.ts
import { useEffect } from 'react';
import { useRecoilState, useResetRecoilState } from 'recoil';
import { signInUserState } from '../store/auth';
import { auth } from './firebase';

/**
 * ユーザー認証する
 */
export const signIn = async (email: string, password: string) => {
	try {
		await auth.signInWithEmailAndPassword(email, password);
	} catch (error) {
		alert('サインイン認証に失敗しました。');
	}
};

/**
 * ユーザー登録する
 */
export const signUp = async (email: string, password: string) => {
	try {
		await auth.createUserWithEmailAndPassword(email, password);
	} catch (error) {
		alert('ユーザー登録に失敗しました。');
	}
};

/**
 * サインアウトする
 */
export const signOut = async () => {
	try {
		await auth.signOut();
	} catch (error) {
		alert('サインアウトに失敗しました。');
	}
};

/**
 * SignInの状態を監視する
 */
export const useAuth = () => {
	const [signInUser, setSignInUser] = useRecoilState(signInUserState);
	const resetStatus = useResetRecoilState(signInUserState);

	useEffect(() => {
		const unSub = auth.onAuthStateChanged(authUser => {
			if (authUser) {
				setSignInUser({
					uid: authUser.uid
				});
			} else {
				resetStatus();
			}
		});
		return () => unSub();
	}, [setSignInUser, resetStatus]);

	return signInUser;
};

```

`useAuth`は、カスタムフックになっています。
`onAuthStateChanged`でユーザーのログイン状態を監視し、ログイン状態なら（authUserが取得できたら）、`setSignInUser`を使ってRecoil Stateにuidを保存します。ログアウト状態なら、`resetStatus`を使ってRecoil Stateの状態を初期化します。

公式ドキュメント

https://firebase.google.com/docs/auth/web/start?hl=ja

### コンポーネント
Recoilを扱うためには、コンポーネントを`RecoilRoot`で囲う必要あります。

```.jsx:src/index.tsx
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import { App } from './components/App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
	<React.StrictMode>
		<RecoilRoot>
			<App />
		</RecoilRoot>
	</React.StrictMode>,
	document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```
Recoilは、コンポーネント全体を`RecoilRoot`で囲っても、Stateの変化に関係するコンポーネントしか再レンダリングされません。

```src/components/App.tsx
import React from 'react';
import { useAuth } from '../firebase/authFunctions';
import { Auth } from './Auth';
import { Main } from './Main';

export const App: React.FC = () => {
	const signInUser = useAuth();

	return <>{signInUser.uid ? <Main /> : <Auth />}</>;
};
```
`App.tsx`でカスタムフックを呼出し、uidの有無で、MainコンポーネントかAuthコンポーネントに遷移させます。

`Main.tsx`と`Auth.tsx`は、割愛します。

<!-- ==================================================== -->
# まとめ
<!-- ==================================================== -->
**Recoil**を使うと、**Redux Tool Kit**を使うより簡単に実装できました。

<!-- ==================================================== -->
# おまけ
<!-- ==================================================== -->
### Authコンポーネントのテンプレート
**Material UI**で`Auth.tsx`を作成する場合、テンプレートを使うと簡単に作成できます。

https://material-ui.com/getting-started/templates/

### ログインユーザー（authUser）から取得できる値
本記事では、uidのみ取得していますが、他にも色々取得できます。

| プロパティ | 型 | 説明 |
|:-----------|:-----------|:-----------|
| uid	| 文字列 | 新しく作成されたユーザーに割り当てる uid。1～128 文字の文字列を指定します。指定されていない場合は、uid が自動的に生成される | 
| email | 文字列 | ユーザーのプライマリ メールアドレス |
| emailVerified | ブール値 | ユーザーのプライマリ メールアドレスが確認されているかどうか。指定されていない場合、デフォルト値は false | 
| phoneNumber | 文字列 | ユーザーのメインの電話番号 | 
| password | 文字列 | ユーザーのハッシュ解除された未加工のパスワード。6 文字以上 | 
| displayName | 文字列 | ユーザーの表示名 | 
| photoURL | 文字列 | ユーザーの写真 URL | 
| disabled | ブール値 | ユーザーが無効かどうか。無効の場合は true、有効の場合は false （デフォルト） | 


https://firebase.google.com/docs/auth/admin/manage-users?hl=ja