Firebase Realtime DatabaseをReactで使ってみる〜書き込み編〜

はじめに

前回読み込みについてまとめたので、今回は書き込み周りについてまとめていく。
基本的なFirebaseの設定については前回行っているので、今回は早速実装 & 必要な設定をしていく。

TL;DR.

コード

実装

FirebaseでGoogleログインできるようにする

WebUIから設定をする。
手順は下記の通り。

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

ログイン処理実装

公式ドキュメント を参考に実装する。

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

ログアウト処理実装

const signOut = () => {
  // signOutを呼び出すだけでOK
  return firebase.auth().signOut();
}

ログイン、ログアウトボタン実装

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

// ログインしているかチェックするカスタムフックを作る
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に渡した値での登録となる。
つまり、既存のデータも含めて渡してあげないと登録済みのデータが消える。

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;
};

// 参考:前回作ったuseDatabase
const useDatabase = () => {
  return useMemo(() => firebase.database().ref('/sample'), []);
};
const useFetchData = (ref: firebase.database.Reference) => {
  const [data, setData] = useState<{ [key: string]: string }>();
  useEffect(() => {
    ref.on('value', snapshot => {
      if (snapshot && snapshot.val()) {
        setData(snapshot.val());
      }
    });
    return () => {
      ref.off();
    };
  }, [ref]);
  return { data };
}

登録フォーム作成

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

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へのログイン周りと登録フォームを新たに追加するのみ。

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

動作確認

ログイン後、keyとvalueのフォームに値を入れ登録ボタンを押す。
無事、フォーム上のリスト部分に値が表示されればOK。

おまけ

部分更新したい場合

更新には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の時と同じく関数を作っていく。

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に更新ボタンを追加する。

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')で指定したパス以下のデータ全てが削除される。

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

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に削除ボタンを追加する。

import React, { useState, ChangeEvent } from 'react';
import { useRegisterData, useUpdateData, useDelteData } from '../firebase/firebaseDB';

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>
  </>;
}

まとめ

前回積み残していた更新分をまとめた。
これで一通りデータの読み書きができるので今度は実際になにか作ってみたいところ。

参考リンク