今回、FirebaseとReactの連携の方法について、基本的な使い方をわかる範囲でまとめました。
Firebaseのデータベース機能Cloud Firestoreを使って簡単なTodoアプリを作成します。
Firebaseとは
まずFirebaseですが、これはGoogleが提供するウェブアプリやモバイルアプリのバックエンドサービス(BaaS)です。
Firebaseのバックエンド機能には、データベースはもちろん、ユーザー認証、ホスティング、アクセス解析などがあります。
BaaSを使うメリットは、サーバーの設定、保守の必要なくアプリを実装できる点だと思います。
作成するTodoアプリ
作成するアプリは簡単なTodo機能だけを持ったシンプルなものです。
タスクの編集、削除ができます。
Source code
今回作成したTodoアプリのコードです。
プロジェクトの準備をする
FirebaseとReactプロジェクトの準備をします。手順は次の通りになります。
- Firebaseのプロジェクト作成
- Firebaseのプロジェクトにウェブアプリを登録
- Cloude Firestoreでデータベースを作成
- Reactのプロジェクトを作成
Firebaseのプロジェクトを作成する
Googleアカウントを準備して、Firebaseにログインします。
コンソール画面で、「プロジェクトを作成」をクリックします。
プロジェクト名を入力します。プロジェクト名は「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のコンソールで表示された接続情報を、変数名を付けて
設定します。
.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で指定したものが存在しない場合は自動で作成してくれます。
今回タイムスタンプも登録していますが、公式ドキュメントで詳しく説明されています。
データを取得する
登録されたTodoのデータを全て取得するにはonSnapshot()を使います。
以下は、登録日時を基準に降順で並び替えたデータを取得する例です。
※dbは、firebase.tsでエクスポートしたCloud Firestoreのオブジェクトです。
データはmap()で1つずつ取り出します。
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のデータ取得方法については、公式ドキュメントで詳しく説明されています。
データを更新する
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のデータ更新方法については、公式ドキュメントで詳しく説明されています。
データを削除する
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のデータ削除の方法については、公式ドキュメントで詳しく説明されています。
まとめ
今回、Fairebaseを使って単純なTodoアプリを作りましたが、認証機能やストレージなどを使えば本格的なアプリも作成できそうです!
本番運営する際は、データベースの書き込み権限やホスティングの設定が必要になります。