Vite + React + Firebaseのハンズオン

## 経緯
皆さん、こんにちは。@inpです。2022年の酷暑の中、如何お過ごしでしょうか？
私は暑さの中、愛ネコと一生延びてます。

さて、実は最近業務でFirebaseをガリガリ使っているアプリの開発に携わっているのですが、リリース済みのサービスということと、Firebaseは初めてなので、実際に自分でAuthenticationを使って実装したことはありませんでした。

そこで、今回はひっさしぶりの記事投稿ですが、０からユーザー登録までを爆速で作っていき備忘録兼、ハンズオン的に残しておこうと思います。

## 登場人物
- Vite
- React
- Typescript
- Firebase
- Firestore
- Functions 


## Viteでプロジェクトを作成
まずはサクッと作りたいので、面倒な作業は全てViteに任せてしまいましょう。

```
$ npm create vite@latest
```
アプリ名を適当に
```
$ Project name >> bakusoku
```
利用するフレームワークを選択。今回はReactなのでReactを選択
```
$ Select a framework: >> - Use arrow-keys. Return to submit.
  vanilla
  vue
> react
  preact
  lit
  svelte
```
せっかくなんでtypescript
```
? Select a variant: » - Use arrow-keys. Return to submit.
    react
>   react-ts
```
あとはViteが作ってくれたディレクトリに移動して、必要なパッケージをインストールして実行。
```
Done. Now run:

  cd bakusoku 
  npm install 
  npm run dev 
```
はい。めちゃめちゃ楽ですね。
![b2d394075dac81f812b05ecec83fb6ca.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/ceae659c-313c-d249-6e05-8d78b43c28af.png)

## Firebase
### プロジェクトの作成
Firebaseコンソールに行って、プロジェクトを追加して、プロジェクトを作成しましょう。
プロジェクトの管理画面の(<>)←こんなアイコンをクリックするとウェブで利用するための手続きが行えます。
適当にアプリ名を入力して、次へをクリックするとクライアントからFirebaseを利用するためのAPIキーなどが入手できるので一旦メモ。
```
// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: "xxx",
  authDomain: "xxx",
  projectId: "xxx",
  storageBucket: "xxx",
  messagingSenderId: "xxx",
  appId: "1:xxx:web:xxx"
};
```

ついでにこのタイミングでターミナルで`$ npm install firebase`コマンドを使ってfirebase追加しておきましょう。

次にFirebaseコンソールメニューからAuthenticationと、Firestore、Functionsを有効にします。

Firebaseコンソール内の「全てのプロダクト」から上の3つを有効に。
また、Functionsは、Blazeプランへの登録が必須みたいです。

（私はもともとBlaze(有料プラン)にして進めたのですが、Blazeじゃない人はエミュでfunctions使えないのでしょうか？）(゜゜)

また、今回は登録時のプロバイダをメールにしておき、メールアドレスとパスワードを有効にしておきます。
![e1f8560bb9f50d37169462893c1ca627.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/011a6a97-9265-0857-ccb9-a753c69cdcb3.png)
![cab14e8558e7a6ac84d4981733455fb5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/f394b63c-da6d-06dc-826b-3ddc56038688.png)

## React
### 環境変数の準備
先ほどコピーさたAPIキー等を環境変数ファイルを作って呼び出せるようにしておきます。
Viteでは環境変数名にVITEをつけないと読み込めないので冗長になりますが、VITE_...としていきます。

```
// .env
VITE_FIREBASE_API_KEY="xxx"
VITE_FIREBASE_AUTH_DOMAIN="xxx"
VITE_FIREBASE_PROJECT_ID="xxx"
VITE_FIREBASE_STORAGE_BUCKET="xxx"
VITE_FIREBASE_MESSAGING_SENDER_ID="xxx"
VITE_FIREBASE_APP_ID="1:xxx:web:xxx"
VITE_FIREBASE_MEASUREMENT_ID="xxx"
```

### firebaseの設定
Firebaseを利用するためのデータベースの設定を入れておきましょう。
```ts
// src/infrastructure/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  mesurementId: import.meta.env.VITE_MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig);

// 認証周り
const auth = getAuth(app);
// firestore
const database = getFirestore(app);

export { database, auth };

```

### データベース操作
ついでにFirestoreにアクセスして動作確認の為ユーザー一覧を取得できるようにしておきます。

```ts
// src/repositories/user.ts
import {
  collection,
  getDocs,
} from "firebase/firestore";
import { database } from "../infrastructure";

const fetchUsers = async (): Promise<UserProps[]> => {
  try {
    const usersRef = collection(database, "users");
    const usersSnapshot = await getDocs(usersRef);

    if (!usersSnapshot.empty) {
      const userList = usersSnapshot.docs.map((snapshot) => {
        const { email } = snapshot.data();
        return { email };
      });

      return userList;
    }
    return [];
  } catch (error) {
    console.error("omg");
    return [];
  }
};

export { fetchUsers };
```

### トップページの改造
ユーザー登録ができるようにしておきます。ついでに分かりやすく登録者一覧を表示できるようにしておきます。firestoreではsnapshotといってデータの数の変更？を検知してくれる機能があるので、それを利用させてもらいます。(のちにFirestoreにユーザーを登録できるようにするので、その時に動くようになります。)


```tsx
import { FC, useEffect, useState } from "react";
import logo from "./logo.svg";
import "./App.css";
import { createUserWithGoogle } from "./utils";
import { collection, onSnapshot, query } from "firebase/firestore";
import { database } from "./infrastructure";
import { fetchUsers } from "./repositories";

const UserEmail: FC<UserProps> = ({ email }) => {
  return <p>{email}</p>;
};

const useObserveUsers = () => {
  const [isMounted, setIsMounted] = useState<boolean>(false);
  const [users, setUsers] = useState<UserProps[]>([]);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  const observeUsers = () => {
    const usersRef = collection(database, "users");
    const _query = query(usersRef);
    const unsubscribe = onSnapshot(_query, (querySnapshot) => {
      querySnapshot.docChanges().forEach((change) => {
        const isAdded = change.type === "added";
        if (!isAdded) return;
        setUsers((previouse) => [
          ...previouse,
          { email: change.doc.data().email },
        ]);
      });
    });
    return unsubscribe;
  };

  useEffect(() => {
    if (isMounted) {
      (async () => {
        const _users = await fetchUsers();
        setUsers(_users);
      })();
      const unsubscribe = observeUsers();
      return () => unsubscribe();
    }
  }, [isMounted]);

  return { users };
};

const App: FC = () => {
  const { users } = useObserveUsers();
  const [authParams, setAuthParams] = useState<AuthProps>({
    email: "",
    password: "",
  });

  const handleInput = (event: React.ChangeEvent<HTMLInputElement>) => {
    const key = event.currentTarget.name;
    setAuthParams((previouse) => ({
      ...previouse,
      [key]: event.target.value,
    }));
  };

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello Vite + React!</p>
        <div>
          <input
            onChange={handleInput}
            type="email"
            placeholder="hoge@example.com"
            name="email"
          />
        </div>
        <div>
          <input
            onChange={handleInput}
            type="password"
            placeholder="hogehoge"
            name="password"
          />
        </div>
        <p>
          <button
            type="button"
            onClick={() => createUserWithGoogle(authParams)}
          >
            create
          </button>
        </p>
        <div>
          {users.length ? (
            users.map((user, index) => (
              <UserEmail key={index} email={user.email} />
            ))
          ) : (
            <p>だれもいないよ</p>
          )}
        </div>
      </header>
    </div>
  );
};

export default App;
```
![f8e52a6f816047f82a7a5215c2cfab3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/929c6e1a-e3c8-a68e-e5cd-fd791eaffa4e.png)

この状態で一度登録をすると、FirebaseConsoleのauthenticationにユーザーが反映されたのが確認できます。
![80f382e8d349086ae5e7208da3fc2cbb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/84369494-2d26-4146-07be-6004d454215d.png)


### emulator
エミュレーターの利用ができるようにします。今回は分かりやすくするためにアプリ配下にemulatorディレクトリを作っています。利用する機能を選択していきます。
```terminal
$ mkdir emulator && cd $_
$ firebase init
// firebaseの利用が初回の場合ログインが必須になります。

? Are you ready to proceed? Yes
? Which Firebase features do you want to set up for this directory? Press Space to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ( ) Realtime Database: Configure a security rules file for Realtime Database and (optionally) provision default instance
 (*) Firestore: Configure security rules and indexes files for Firestore
>(*) Functions: Configure a Cloud Functions directory and its files
 ( ) Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
 ( ) Hosting: Set up GitHub Action deploys
 ( ) Storage: Configure a security rules file for Cloud Storage
 (*) Emulators: Set up local emulators for Firebase products
(Move up and down to reveal more choices)

? Please select an option: Don't set up a default project

=== Functions Setup

? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? Yes
+  Wrote functions/package.json
+  Wrote functions/.eslintrc.js
+  Wrote functions/tsconfig.json
+  Wrote functions/tsconfig.dev.json
+  Wrote functions/src/index.ts
+  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. (Press <space> 
to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
>(*) Authentication Emulator
 (*) Functions Emulator
 (*) Firestore Emulator
 ( ) Database Emulator
 ( ) Hosting Emulator
 ( ) Pub/Sub Emulator
 ( ) Storage Emulator

? Which port do you want to use for the auth emulator? 9099        
? Which port do you want to use for the functions emulator? 5001   
? Which port do you want to use for the firestore emulator? 8080
? Would you like to enable the Emulator UI? Yes
? Which port do you want to use for the Emulator UI (leave empty to use any available port)? 
? Would you like to download the emulators now? Yes
```

functionsの動作確認を行えるように1回ビルドします。src/index.tsのコメントを解除してビルド後、エミュレーターを起動。

```ts
import * as functions from "firebase-functions";

// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript

export const helloWorld = functions.https.onRequest((request, response) => {
  functions.logger.info("Hello logs!", {structuredData: true});
  response.send("Hello from Firebase!");
});

```

```terminal
$ cd functions
$ npm run build
$ cd ../
$ firebase emulators:start
```
起動に成功したら[http://localhost:4000](http://localhost:4000)にアクセスするとエミュレーターの画面が表示できます。
![46b2d33432530f27b75c10ae41ed5ffc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/f353cffd-780c-90c8-488b-ceea717bac79.png)

ついでに、curlしてhelloworldができるか確認。
```terminal
$ curl http://localhost:5001/xxx/us-central1/helloWorld
Hello from Firebase!
```

### emulatorに繋がるように
次に開発環境ではエミュレーターに繋がるようにしておきます。

```ts
import { initializeApp } from "firebase/app";
import { connectAuthEmulator, getAuth } from "firebase/auth";
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";


const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
  mesurementId: import.meta.env.VITE_MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig);

// 認証周り
const auth = getAuth(app);

// firestore
const database = getFirestore(app);

// 開発環境ではemulatorに接続
const isDevelopment = import.meta.env.DEV;
if(isDevelopment) {
  connectAuthEmulator(auth, 'http://localhost:9099');
  connectFirestoreEmulator(database, '127.0.0.1', 8080);
}

export { database, auth };
```

この状態で、メールアドレスとパスワードを入力して、ローカルに立っているAuthenticationを確認すると。
![d71afb549943f6c9234edbeb89c435cb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/11ec4971-bccd-8516-ba91-abbe958d2e45.png)
はい。良い感じですね。

### functionsを書く
次に、ユーザー登録時に登録イベントを検知して、FunctionsからFirestoreにユーザーを追加するようにしておきます。

```ts
// src/index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";

const app = admin.initializeApp(functions.config().firebase);
const firestore = app.firestore();

// Start writing Firebase Functions
// https://firebase.google.com/docs/functions/typescript

// export const helloWorld = functions.https.onRequest((request, response) => {
//   functions.logger.info("Hello logs!", {structuredData: true});
//   response.send("Hello from Firebase!");
// });

export const createUserInFirebase = functions.auth
    .user()
    .onCreate((userRecord) => {
      try {
        const usersRef = firestore.collection("users");
        usersRef.doc(userRecord.uid).set({
          email: userRecord.email,
        });
      } catch (error) {
        console.error(error);
      }
    });
```

そしてビルドを挟んで、もう一度ユーザーを作成してみます。

```terminal
$ npm run build
```
![94e319eb727f35392b3cf6331e1765bd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/965b0eae-d29e-f3b4-66a2-64b404e88fc5.png)

はい。良い感じに追加されていますね。

### 実行してみる

![2acd6844d91e63a1531aa53a1989629d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126631/2b6300f4-da68-dfae-647f-1b712dfa6e70.gif)

めちゃめちゃ簡単に認証機能が実装できました！👍
今回はかなり端折って書いているのですが、本番で動かすときはfirestoreのルールなども書いていかないといけないのですが、その当たりもエミュレーターでできるのはいいですよね。
また、エミュレーター上だとFirestoreへのアクセスログも確認できるので、ボトルネックなどの発見などにも良さそうです。