Firebase Realtime DatabaseをReactで使ってみる

# はじめに
Firebase Realtime DatabaseをReact使ってみた今日このごろ。
備忘録も兼ねて使い方をまとめておく。
今回はReact(クライアント側)の設定だけで、Firebaseのプロジェクトの設定などは割愛する。
※一応やったことはWebUIポチポチしただけ。

# TL;DR.
[作ったコード](https://github.com/Tetsuya-Minase/program-samples/tree/master/realtime-db-react)

# Reactプロジェクト作成
`create-react-app`からスタートする。
サクッと用意。

```bash
$ npx create-react-app realtime-db-react --template typescript
```

# Firebase周りの設定
## プロジェクトの準備
FirebaseCLIを使って設定を進める。
設定は[公式のリファレンス](https://firebase.google.com/docs/cli)を見ながら行う。
また、プロジェクトは作ってある前提とする。

```bash
# ログインしていなければログインする
$ npx firebase login

# Firebaseプロジェクト初期化
$ npx firebase init
# 設定した内容は下記の通り
>? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
❯◉ Database: Deploy Firebase Realtime Database Rules

? Please select an option: 
❯ Use an existing project

? Select a default Firebase project for this directory: 
❯ 作成しておいたプロジェクト

# デフォルトのまま
? What file should be used for Database Rules? database.rules.json

✔  Firebase initialization complete!
```

## DBのルール変更
上記手順で作ったままではDBに読み書きできないようになっているので少し修正する。
生成されている`database.rules.json`を下記のように修正。

```json
{
  "rules": {
		// 読むのは誰でも可能
    ".read": true,
		// 書くのは認証済みの場合のみ
    ".write": "auth != null"
  }
}
```

## ルール反映
json変えたではもちろん反映されないので、`firebase deploy`コマンドを叩いて反映する。
反映できているかどうかは下記手順で確認できる。
表示されるURLからコンソールを表示
→左カラムにある`Database`を選択
→メインカラム上部のプルダウンから`Realtime Database`を選択
→ルールタブを表示

```bash
$ npx firebase deploy
# ↓が表示されればOK
✔  Deploy complete!
Project Console: https://console.firebase.google.com/project/hogehoge/overview
```

## テスト用データ追加
確認用にWebUIからデータ追加しておく。
追加したのは↓の感じ。

```json
{
	"sample": {
		"key1": "value1",
		"key2": "value2"
	}
}
```

# データを取得できるようにする
まずは登録したデータを取得するところまでやる。

## Firebase SDKとFirebase Admin SDKのインストール
アプリからFirebaseを使うために必要なライブラリをインストールする。
adminの方は認証がいる場合に必要なのでどちらも入れておく。

```bash
$ yarn add firebase firebase-admin
```

## 初期化周り
初期化に必要な情報をWebUIから取ってくる。
プロジェクトのページ左カラムにある歯車をクリック
→プロジェクトの設定をクリック
→Settingsページ、全般タブ下側にあるマイアプリにある`</>`マークをクリック
→ウェブアプリにFirebaseを追加のページに飛ぶので、お好みの名前を入れて登録をクリック(hostingはお好みで。今回はやらない)
→スクリプトが表示されるので、`firebaseConfig`の内容だけコピーする

必要な情報を取れたのでコードを書いていく。
初期化はこれでOK

```typescript
import firebase from 'firebase/app';
// 認証周りやDB周りで必要なためimportしておく
import 'firebase/auth';
import 'firebase/database';

// コピーしてきたfirebaseConfigそのまま
// 元がvarで宣言されているので、constに変更
const firebaseConfig = {
	// コピペ
};

firebase.initializeApp(firebaseConfig);
export { firebase };
```

## データベースに接続する
データのやり取りは`firebase.database.Reference`を通してやり取りされる。
今回は決まったパスのデータを取得したいので、パスを指定した上でメモ化しておく。

```tsx
// カスタムフックにしておく
const useDatabase = () => {
  // 同じパスでは毎回同じ結果が得られるのでmemo化しておく
  return useMemo(() => firebase.database().ref('/sample'), []);
};
```

## データを取得する
↑で作ったReferenceを受け取る関数を作成する。
Referenceのイベントをlistenすることでデータを取得できる。
指定したパスのデータに対する更新をすべて検知するには`value`を指定すれば良い。

```tsx

// hooksを使いたいのでカスタムhooksにしておく
const useFetchData = (ref: firebase.database.Reference) => {
  const [data, setData] = useState<{[key: string]: string}>();
  useEffect(() => {
		// イベントリスナーを追加するにはonを使う
    ref.on('value', snapshot => {
			// パスに対する全データを含むsnapshotが渡される
			// ない場合はnullが変えるので存在をチェックしておく
      if (snapshot?.val()) {
        setData(snapshot.val());
      }
    });
    return () => {
      ref.off();
    };
	// refの変更に応じて再取得する
  }, [ref]);
	// データを返却する
  return { data };
}

// 実際に呼び出す際はこちらを使う
export const useFetchAllData = () => {
	// refを取得して
  const ref = useDatabase();
	// ref渡してデータを取得する
  return useFetchData(ref);
};
```

## Componentからデータ取得する
`useFetchAllData`を使ってデータを取ってくるコンポーネントを作成する。
取得したデータはobject形式なので、list形式に変換してから表示する。

```tsx

import React, { useMemo } from 'react';
import { useFetchAllData } from '../firebase/firebaseDB';

export const ListComponent: React.FC = () => {
	// dataを取ってくる
  const { data } = useFetchAllData();
	// object形式なので使いやすいように{key, value}形式のリストに変換する
	// また、データが変わらない限り結果は同じなのでメモ化しておく
  const dataList = useMemo(() => Object.entries(data || {}).map(([key, value]) => ({ key, value })), [data]);

  return <dl>{dataList.map(({ key, value }) =>
    <React.Fragment key={`${key}${value}`}>
      <dt>key: {key}</dt>
      <dt>value: {value}</dt>
    </React.Fragment>
  )}</dl>
};
```

起動した際にデータを表示できれば取得はOK。

# データを登録できるようにする
次にデータの登録ができるようにする。
FirebaseのWebUIで設定が必要なのでそこから始める。

## FirebaseでGoogleログインできるようにする
WebUIから設定をする。
手順は下記の通り。
左カラムのAuthenticationをクリック
→デフォルトでUsersタブにいると思うので、そのまま表示されている`ログイン方法を設定`ボタンクリック
→一覧の中からGoogleを選択
→右上のトグルを有効に切り替える
→`プロジェクトのサポートメール`にメールアドレスを入力して保存をクリック

## ログイン処理実装
[公式ドキュメント](https://firebase.google.com/docs/auth/web/google-signin?hl=ja)を参考に実装する。

```tsx

const signInWithPopup = () => {
	// Googleプロバイダオブジェクトのインスタンスを作成
  const googleAuthProvider = new firebase.auth.GoogleAuthProvider();
	// 別タブでログイン画面に飛ばしたいため、signInWithPopupを使う
	// リダイレクトでログイン画面に飛ばしたい場合はsignInWithRedirectを使う
  return firebase.auth().signInWithPopup(googleAuthProvider);
}
```

## ログアウト処理実装
```tsx
const signOut = () => {
	// signOutを呼び出すだけでOK
  return firebase.auth().signOut();
}
```

## ログイン、ログアウトボタン実装
ログイン、ログアウトは↑の処理で良いが使いやすい用にボタンにする。
合わせてログイン/ログアウトの判定をするため、ログインしているかどうかを確認する処理も追加する。

```tsx

// ログインしているかチェックするカスタムフックを作る
const useFirebaseLogin = () => {
	// stateでログイン状態を保持
  const [loggedin, setLoggedin] = useState(false);
  useEffect(() => {
		// 現在ログインしているユーザを取得
    firebase.auth().onAuthStateChanged(user => {
			// ユーザ情報が取れればログイン状態
      setLoggedin(!!user);
    });
  }, [])
	// ログイン情報を返却
  return loggedin;
};

// ログイン、ログアウトボタンを作る
export const FirebaseAuthComponent: React.FC = () => {
  const loggedin = useFirebaseLogin();
  if (!loggedin) {
		// ログインしていなければログインボタンを表示
    return <button onClick={() => signInWithPopup()}>ログイン</button>;
  }
	// ログインしているならログアウトボタンを表示
  return <button onClick={() => signOut()}>ログアウト</button>;
}
```

## 登録処理作成
Realtime Databaseに登録するための処理を作成する。
登録には`firebase.database.Reference.set()`を使えば良い。
ただしこの登録はsetに渡した値での登録となる。
つまり、既存のデータも含めて渡してあげないと登録済みのデータが消える。

```tsx
const useSetDocument = (ref: firebase.database.Reference) => {
  const updateDocument = useCallback(
    (document: unknown) => {
			// refについては前回の記事参照
			// setに登録したいデータを渡してあげれば登録できる
      ref.set(document);
    }, [ref]
  );
  return updateDocument;
};

export const useRegisterData = () => {
	// 前回作ったuseDatabase()を使いref取得
  const ref = useDatabase('');
  const setDocument = useSetDocument(ref);
	// 登録済みのデータを全部取得する
  const {data: registeredData} = useFetchAllData();

	// データを登録する関数を返却する
  const registerData = useCallback((registerData: { [key: string]: string }) => {
		// 既存のデータと登録するkey-valueを合わせて登録関数に渡す
    setDocument({...registeredData, ...registerData});
  }, [setDocument, registeredData]);

  return registerData;
};
```

## 登録フォーム作成
key-valueを登録するフォームを作成する。
今回は登録できることだけを目的として一旦必要な諸々は見なかったことにする。

```tsx
export const FormComponent: React.FC = () => {
	// データを登録する関数
	// ↑で作成したuseRegisterDataを使う
	const registerData = useRegisterData();
  const [keyData, setKeyData] = useState<string>('');
  const [valueData, setValueData] = useState<string>('');
  return <>
		{/* 今回は登録できれば良いとして、keyとvalueについてそれぞれ登録するinputフォームを作る */}
    <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label>
    <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label>
    {/* 登録ボタンを押したときにsetDocument関数を呼び出してデータを追加する */}
		<button onClick={() => setDocument({[keyData]: valueData})}>登録</button>
  </>;
}
```

## App.tsxを修正
前回、何もしないでListComponentを呼び出すだけにしてあった`App.tsx`を修正する。
修正はfirebaseへのログイン周りと登録フォームを新たに追加するのみ。

```tsx
const App: React.FC = () => {
  return (
  <>
    <FirebaseAuthComponent />
    <ListComponent />
    <FormComponent />
  </>
  );
}
```

# おまけ
取得、登録ができるところまで行った。
おまけとして部分更新や削除の場合もまとめる。

## 部分更新したい場合
更新には`firebase.database.Reference.update()`を使う。
setを使うと`firebase.database().ref('/sample')`で指定したパス以下全てが更新されてしまうが、updateを使うと指定したパス以下でkeyが一致するオブジェクトをのみを変更できる。

`{"key1": "value1", "key2": "value2"}`というデータが登録されている状態で、`update({key1: 'value2'})` というデータを渡してupdateを呼び出すと`{"key1": "value2", "key2", "value2"}`というように部分的に更新ができる。
一致するキーが無ければ新規登録になる。
(なので、1件ずつ登録する今回のサンプルではsetではなく、updateを使っても変わらない)

setの時と同じく関数を作っていく。

```tsx
const useUpdateDocument = (ref: firebase.database.Reference) => {
	// ref.updateがObjectを受け取るので、Objectを引数に取る関数を定義
  const updateDocument = useCallback((document: Object) => ref.update(document), [ref]);
  return updateDocument;
}

export const useUpdateData = () => {
  // setの時と同じくrefを取得して、
	const ref = useDatabase();
	// 関数呼び出して
  const updateDocument = useUpdateDocument(ref);
	// 更新処理を作成する
  const updateData = useCallback((registerData: {[key: string]: string}) => {
    updateDocument(registerData);
  }, [updateDocument]);
  return updateData;
}
```

登録フォームのComponentに更新ボタンを追加する。

```tsx
export const FormComponent: React.FC = () => {
  const registerData = useRegisterData();
	// 更新処理を呼び出す
  const updateData = useUpdateData();

  const [keyData, setKeyData] = useState<string>('');
  const [valueData, setValueData] = useState<string>('');

  return <>
    <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label>
    <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label>
    <button onClick={() => registerData({[keyData]: valueData})}>登録</button>
    {/* ボタン押したときに対象のデータを更新 */}
		<button onClick={() => updateData({[keyData]: valueData})}>更新</button>
  </>;
}
```

## 削除したい場合
削除には`firebase.database.Reference.remove()`を使う。 
remove()を使うと`firebase.database().ref('/sample')`で指定したパス以下のデータ全てが削除される。

こちらも同じく関数を作っていく。

```tsx
const useRemoveDocument = (ref: firebase.database.Reference) => {
	// 特に引数が必要ないのでただ呼び出すのみ
  const deleteDocument = useCallback(() => ref.remove(), [ref]);
  return deleteDocument;
}
// set、updateと同じなので割愛
export const useDelteData = () => {
  const ref = useDatabase();
  const removeDocument = useRemoveDocument(ref);
  const deleteData = useCallback(() => removeDocument(), [removeDocument])
  return deleteData;
}
```

同じく登録フォームのComponentに削除ボタンを追加する。

```tsx
export const FormComponent: React.FC = () => {
  const registerData = useRegisterData();
  const updateData = useUpdateData();
	// 削除処理を呼び出す
  const deleteData = useDelteData();

  const [keyData, setKeyData] = useState<string>('');
  const [valueData, setValueData] = useState<string>('');

  return <>
    <label>Key: <input placeholder="key" onChange={(event: ChangeEvent<HTMLInputElement>) => setKeyData(event.target.value)}/></label>
    <label>Value: <input placeholder="value" onChange={(event: ChangeEvent<HTMLInputElement>) => setValueData(event.target.value)}/></label>
    <button onClick={() => registerData({[keyData]: valueData})}>登録</button>
    <button onClick={() => updateData({[keyData]: valueData})}>更新</button>
    {/* ボタン押したときに対象のデータを全消し */}
    <button onClick={() => deleteData()}>全消し</button>
  </>;
}
```

# まとめ
Firebaseを使って一通り読み込み、登録、更新ができるところまでやった。
今度は実際に何かを作ってみたいところ。

# 参考サイト
- [Firebase CLI リファレンス](https://firebase.google.com/docs/cli)
- [Firebase を JavaScript プロジェクトに追加する](https://firebase.google.com/docs/web/setup?hl=ja)
- [サーバーに Firebase Admin SDK を追加する](https://firebase.google.com/docs/admin/setup?hl=ja)
- [ウェブでのデータの取得 | Firebase](https://firebase.google.com/docs/database/web/retrieve-data?hl=ja)
- [ウェブでのデータの読み取りと書き込み | Firebase Realtime Database](https://firebase.google.com/docs/database/web/read-and-write?hl=ja)
- [Firebase Authentication](https://firebase.google.com/docs/auth?hl=ja)
- [JavaScript で Google ログインを使用して認証する | Firebase](https://firebase.google.com/docs/auth/web/google-signin?hl=ja)
- [Firebase でユーザーを管理する](https://firebase.google.com/docs/auth/web/manage-users?hl=ja)