FirebaseとReactの連携についてまとめました【Cloud Firestore/v9/Todoアプリ/TypeScript】

JavaScript

今回、FirebaseとReactの連携の方法について、基本的な使い方をわかる範囲でまとめました。
Firebaseのデータベース機能Cloud Firestoreを使って簡単なTodoアプリを作成します。

Firebaseとは

まずFirebaseですが、これはGoogleが提供するウェブアプリやモバイルアプリのバックエンドサービス(BaaS)です。
Firebaseのバックエンド機能には、データベースはもちろん、ユーザー認証、ホスティング、アクセス解析などがあります。

BaaSを使うメリットは、サーバーの設定、保守の必要なくアプリを実装できる点だと思います。

BaaSとは『Backend as a Service』の略です。バックエンド機能がクラウドベース(API)として提供されることを指します。

作成するTodoアプリ

作成するアプリは簡単なTodo機能だけを持ったシンプルなものです。
タスクの編集、削除ができます。

Source code

今回作成したTodoアプリのコードです。

GitHub - matsu0314/firebase-todo
Contribute to matsu0314/firebase-todo development by creating an account on GitHub.

プロジェクトの準備をする

FirebaseとReactプロジェクトの準備をします。手順は次の通りになります。

  1. Firebaseのプロジェクト作成
  2. Firebaseのプロジェクトにウェブアプリを登録
  3. Cloude Firestoreでデータベースを作成
  4. Reactのプロジェクトを作成

Firebaseのプロジェクトを作成する

Googleアカウントを準備して、Firebaseにログインします。

Firebase | Google’s Mobile and Web App Development Platform
Firebase は、デベロッパーがユーザーに人気のアプリやゲームを開発できるよう支援する Google のモバイルおよびウェブアプリ開発プラットフォームです。

コンソール画面で、「プロジェクトを作成」をクリックします。

プロジェクト名を入力します。プロジェクト名は「firebase-todo」にしました。

Googleアナリティクスを有効にするか選択します。
※今回はアクセス解析の必要がないので、有効化していません。

以下の画面が表示されればプロジェクトの作成が完了です

Firebaseプロジェクトにアプリを追加

コンソール画面に戻ったら、Firebaseプロジェクトにアプリを追加します。
今回は「ウェブ」をクリックします。

ウェブアプリに名前をつけます。
好きな名前で良いのですが、今回はプロジェクトと同じ名前にしました。

ウェブアプリを登録すると、次のようにアプリとの連携に必要な情報が表示されます。

①npmコマンドで必要なモジュールをインストール
プロジェクトにFirebaseモジュールをインストールする際に使用するコマンドです。後ほどReactで作るTodoアプリのプロジェクトにインストールします。

②アプリ連携に必要な接続情報
アプリ連携に必要な接続情報が記載されています。セキュリティ面から一般に公開しない情報となります。※キャプチャのアプリは既に削除しています。
Reactアプリとの連携の際は、環境変数として.envファイルから読み込むようにします。

これで、Firebaseのプロジェクトが作成できました!

Cloud Firestoreでデータベースを作成する

次は、アプリのTodoデータを保存するデータベースを作成します。
コンソールでCloud Firestoreを選択して、データベースの作成をクリックします。

データベース作成の際、本番モードかテストモード、どちらかを選択します。
主な違いはデータベースの書き込み権限の違いです。今回は、練習なのでテストモードで開始します。

テストモードのセキュリティルールは、30日間、誰でもデータベースの表示、編集、削除ができます。

次の画面では、データベースのロケーションを設定します。一応Tokyoにしました。
ロケーションを選択したら「有効にする」をクリックします。

以上でデータベースが作成できました!

Reactのプロジェクトを作成する

ここからはターミナルでの操作になります。
まずReactアプリのプロジェクトを作成します。今回のプロジェクト名は『firebase-todo』です。

プロジェクトを作成したいディレクトリにcdで移動したら、次のコマンドを実行してReactプロジェクトを作成します。

npx create-react-app firebase-todo --template typescript

今回、TypeScriptを使用するので、オプションに–template typescriptをつけます。

firebaseモジュールをインストールする

firebaseモジュールをプロジェクトにインストールします。

$ npm install firebase

バージョンは以下の通りです。

$ firebase --version
11.22.0

以上でプロジェクトの準備は完了です!

Todoアプリを作成する

ここからReactでTodoアプリを作っていきます。

ファイル構成

アプリのファイル構成は次のようになります。

.
├── README.md
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
│   ├── App.css
│   ├── App.tsx
│   ├── components # Todoアプリのコンポーネント
│   │   ├── TodoInput.tsx
│   │   ├── TodoItem.tsx
│   │   └── TodoList.tsx
│   ├── firebase.ts # Firebaseのconfを管理
│   ├── index.css
│   ├── index.tsx
│   └──logo.svg
├── .env # 環境変数を管理
└── tsconfig.json

今回、特に重要なファイルは、firebase.tsです。
Firebaseとの連携に必要な接続情報をまとめたファイルになります。

プロジェクトルートに.envファイルを作成する

.envファイルは、アプリケーションの環境変数を定義するためのファイルです。プロジェクト直下に置いてある.envファイルは自動で読み込まれ、コードから参照できます。

Firebaseのコンソールで表示された接続情報を、変数名を付けて設定します。

REACT_APP_<変数名>=”<接続情報(APIキー)>

.envファイルは次のようになります。
※REACT_APP_FIREBASE_APP_IDの「=」の後に改行がありますが、実際は改行なしで設定しています。

REACT_APP_FIREBASE_APIKEY="AIzaSyB-AoOSu*****IGGbwQ_U4qM28****9Hc"
REACT_APP_FIREBASE_DOMAIN="fir-todo-6f**b.firebaseapp.com"
REACT_APP_FIREBASE_PROJECT_ID="fir-todo-6f**b"
REACT_APP_FIREBASE_STORAGE_BUCKET="fir-todo-6f**b.appspot.com"
REACT_APP_FIREBASE_SENDER_ID="7151278***27"
REACT_APP_FIREBASE_APP_ID="1:7151278***27:web:3ed34242a6a767d***a326"

.envファイルは通常、開発環境と本番環境の両方で使用されますが、ファイル名を「.env.local」にすれば開発環境に優先的に読み込まれます。

Firebaseを初期化する

環境変数を用意したら、Firebase接続情報を設定するfirebase.tsファイルを作成します。
先程の環境変数を使って、Firebaseオブジェクトを初期化します。

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_APIKEY,
  authDomain: process.env.REACT_APP_FIREBASE_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);
// db情報をexport
export const db = getFirestore(app);

最後に、セットアップしたCloud Firestoreのオブジェクトを変数名「db」でエクスポートしておきます。

export const db = getFirestore(app);

データを追加する

データをCloud Firestoreに登録するにはaddDoc()を使います。
構文は次の通りです。
※dbは、firebase.tsでエクスポートしたCloud Firestoreのオブジェクトです。

addDoc(collection(db, <コレクション名>),  <登録する値(オブジェクト)> );

フォームに入力した値をCloud Firestoreに登録するコードは次のようになります。
※関係のある箇所を色付けしました。

import { useState } from 'react';
import { db } from '../firebase';
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';

const TodoInput: React.FC = () => {
  const [inputText, setInputText] = useState('');

  // TODO追加
  const onSubmitAdd = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (inputText === '') return;
    await addDoc(collection(db, 'todos'), {
      text: inputText,
      timestamp: serverTimestamp(),
    });
    setInputText('');
  };
  return (
    <form onSubmit={onSubmitAdd} style={{ display: 'inline' }}>
      <input onChange={(e) => setInputText(e.target.value)} value={inputText} />
      <button>追加</button>
    </form>
  );
};

export default TodoInput;

実際にTodoを登録した後にコンソール画面を確認すると、次のようにCloud Firestoreに値が登録されています。
コレクション名は「todo」で、IDはランダムな値が割り振られています。

登録の際、本来はIDを設定する必要がありますが、addDoc()はIDを自動生成してくれます。
IDを手動で設定したい場合は、setDoc()を使います。また、collectionで指定したものが存在しない場合は自動で作成してくれます。

今回タイムスタンプも登録していますが、公式ドキュメントで詳しく説明されています。

Cloud Firestore にデータを追加する  |  Firebase

データを取得する

登録されたTodoのデータを全て取得するにはonSnapshot()を使います。

以下は、登録日時を基準に降順で並び替えたデータを取得する例です。
※dbは、firebase.tsでエクスポートしたCloud Firestoreのオブジェクトです。

データはmap()で1つずつ取り出します。

const q = query(collection(db, ‘todos’), orderBy(‘timestamp’, ‘desc’));

onSnapshot(q, async (snapshot) => {snapshot.docs.map((doc) => ({doc.id})))

Todoのデータを全て取得するコードは次のようになります。
※関係のある箇所を色付けしました。

import { useState, useEffect } from 'react';
import { db } from '../firebase';
import { collection, query, onSnapshot, orderBy } from 'firebase/firestore';
import TodoItem from './TodoItem';

type TodoListType = {
  id: string;
  text: string;
  timestamp: any;
};

const TodoList: React.FC = () => {
  const [todos, setTodos] = useState<TodoListType[]>([
    { id: '', text: '', timestamp: null },
  ]);
  useEffect(() => {
    const q = query(collection(db, 'todos'), orderBy('timestamp', 'desc'));
    const unSub = onSnapshot(q, async (snapshot) => {
      setTodos(
        snapshot.docs.map((doc) => ({
          id: doc.id,
          text: doc.data().text,
          timestamp: doc.data().timestamp,
        }))
      );
    });

    return () => {
      unSub();
    };
  }, []);

  return (
    <>
      {todos[0]?.id && (
        <>
          {todos.map((todo) => (
            <TodoItem key={todo.id} todo={todo} />
          ))}
        </>
      )}
    </>
  );
};

export default TodoList;

Reactで外部データを取得する際は、useEffect()を使用します。読み込み時に1回だけデータを取得するのがポイントです。

Cloud firestoeのデータ取得方法については、公式ドキュメントで詳しく説明されています。

リアルタイム アップデートを入手する  |  Firestore  |  Google Cloud

データを更新する

Todoのデータを更新するにはupdateDoc()を使います。
構文は次の通りです。
※dbは、firebase.tsでエクスポートしたCloud Firestoreのオブジェクトです。

updateDoc(doc(db, <コレクション名>, <ID>)

Todoのデータを更新するには対象のタスクをダブルクリックします。
コードは次のようになります。※関係のある箇所を色付けしました。

import { useState, useEffect, useRef } from 'react';
import { db } from '../firebase';
import { doc, deleteDoc, updateDoc } from 'firebase/firestore';

type TodoItemType = {
  todo: { id: string; text: string; timestamp: any };
};

const TodoItem: React.FC<TodoItemType> = (props) => {
  const { id, text, timestamp } = props.todo;

  const [update, setUpdate] = useState('');
  const [isEdit, setIsEdit] = useState(false);
  const updateInput = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // 選択したアイテムにフォーカスを当てる
    const refInput = updateInput.current;
    if (isEdit === true) {
      if (refInput === null) return;
      refInput?.focus();
    }
  }, [isEdit]);

  const onSubmitUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    updateItem(id);
  };
  const updateItem = async (id: string) => {
    if (update === '') return;
    await updateDoc(doc(db, 'todos', id), {
      text: update,
    });
    setIsEdit(false);
  };
  const deleteItem = async (id: string) => {
    await deleteDoc(doc(db, 'todos', id));
  };

  return (
    <li className="todo-item">
      {isEdit === false ? (
        <div onDoubleClick={() => setIsEdit(true)}>
          <span>{text}</span>
          <span className="date-text">
            {new Date(timestamp?.toDate()).toLocaleString()}
          </span>
        </div>
      ) : (
        <div>
          <form onSubmit={onSubmitUpdate}>
            <input
              type="text"
              className="update-input"
              placeholder={text}
              ref={updateInput}
              onChange={(e) => setUpdate(e.target.value)}
            />
            <button className="updateBtn" onClick={() => updateItem(id)}>
              更新
            </button>
          </form>
        </div>
      )}

      <button className="deleteBtn" onClick={() => deleteItem(id)}>
        削除
      </button>
    </li>
  );
};

export default TodoItem;

Cloud firestoeのデータ更新方法については、公式ドキュメントで詳しく説明されています。

Cloud Firestore にデータを追加する  |  Firebase

データを削除する

Todoのデータを削除するにはdeleteDoc()を使います。
構文は次の通りです。
※dbは、firebase.tsでエクスポートしたCloud Firestoreのオブジェクトです。

deleteDoc(doc(db, <コレクション名>, <ID>));

Todoのデータを削除するには「削除」ボタンをクリックします。コードは次のようになります。
※関係のある箇所を色付けしました。

import { useState, useEffect, useRef } from 'react';
import { db } from '../firebase';
import { doc, deleteDoc, updateDoc } from 'firebase/firestore';

type TodoItemType = {
  todo: { id: string; text: string; timestamp: any };
};

const TodoItem: React.FC<TodoItemType> = (props) => {
  const { id, text, timestamp } = props.todo;

  const [update, setUpdate] = useState('');
  const [isEdit, setIsEdit] = useState(false);
  const updateInput = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // 選択したアイテムにフォーカスを当てる
    const refInput = updateInput.current;
    if (isEdit === true) {
      if (refInput === null) return;
      refInput?.focus();
    }
  }, [isEdit]);

  const onSubmitUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    updateItem(id);
  };
  const updateItem = async (id: string) => {
    if (update === '') return;
    await updateDoc(doc(db, 'todos', id), {
      text: update,
      // timestamp: serverTimestamp(),
    });
    setIsEdit(false);
  };
  const deleteItem = async (id: string) => {
    await deleteDoc(doc(db, 'todos', id));
  };

  return (
    <li className="todo-item">
      {isEdit === false ? (
        <div onDoubleClick={() => setIsEdit(true)}>
          <span>{text}</span>
          <span className="date-text">
            {new Date(timestamp?.toDate()).toLocaleString()}
          </span>
        </div>
      ) : (
        <div>
          <form onSubmit={onSubmitUpdate}>
            <input
              type="text"
              className="update-input"
              placeholder={text}
              ref={updateInput}
              onChange={(e) => setUpdate(e.target.value)}
            />
            <button className="updateBtn" onClick={() => updateItem(id)}>
              更新
            </button>
          </form>
        </div>
      )}

      <button className="deleteBtn" onClick={() => deleteItem(id)}>
        削除
      </button>
    </li>
  );
};

export default TodoItem;

Cloud firestoeのデータ削除の方法については、公式ドキュメントで詳しく説明されています。

Cloud Firestore からデータを削除する  |  Firebase

まとめ

今回、Fairebaseを使って単純なTodoアプリを作りましたが、認証機能やストレージなどを使えば本格的なアプリも作成できそうです!
本番運営する際は、データベースの書き込み権限やホスティングの設定が必要になります。