【最短実装】React+TS+Firestoreで簡単なCRUDアプリを実装する手順まとめ

`create-react-app` + Firestore を使って、簡易的なCRUDアプリを実装する手順を解説します。極力最小構成にしているので、これから理解しようとしている方にはちょうど良いと思います！

ReactとFirestoreについてなんとなく理解している方が対象です。

## ソースコード

コミット分けたので、ログの差分をご活用ください！

https://github.com/kazztech/react-ts-firebase/commits/master

## フロントの実装

### React(TS)プロジェクトを作成

nodejsの環境は構築済みとします。

```bash
# npx create-react-app <プロジェクト名> [オプション]
npx create-react-app react-ts-firebase --typescript
```

### 開発環境で実行

```bash
cd <プロジェクト名>
npm run start
```

`http://localhost:3000` へアクセス

![スクリーンショット 2020-09-23 14.35.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/338b8f21-441c-b67b-21f2-f8056bed1252.png)


### 不要なCSSなどのファイルを削除

```
./
├── node_modules
│   └── ...
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.tsx
│   ├── index.tsx
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
├── tsconfig.json
└── yarn.lock
```

### 一旦余計なソースを削除


```tsx:index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(<App />, document.getElementById("root"));

serviceWorker.unregister();
```

```tsx:App.tsx
import React from "react";

function App() {
  return <div></div>;
}

export default App;
```

### まずはダミーのデータを用いて描画処理

今回はAppコンポーネントに直書きします。

```tsx:App.tsx
import React, { useState } from "react";

type User = {
  id: string;
  name: string;
  height: number;
};

function App() {
  const [users, setUsers] = useState<User[]>([
    { id: "ID1", name: "ゾンビ", height: 195 },
    { id: "ID2", name: "スケルトン", height: 199 },
    { id: "ID3", name: "クリーパー", height: 170 },
  ]);
  return (
    <div>
      <h2>Users</h2>
      {users.map((user) => (
        <div key={user.id}>
          {user.name} ({user.height}cm)
        </div>
      ))}
    </div>
  );
}

export default App;
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/7109fc81-3610-d82d-c332-28f2d2adb4bf.png" width="400px"/>

## Firestoreの登録

https://console.firebase.google.com/

登録を進めたら、

プロジェクトを追加 -> プロジェクト名を入力 -> その他入力 -> 完了

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/7025502d-5c58-e34d-4571-981a15cf087b.png" width="400px"/>


サイドバーから「Cloud Firestore」を選択 -> データベース作成 -> テストモードをチェック -> Cloud Firestore のロケーション「asia-northeast3」を選択 -> 有効にする -> 完了

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/028fc59a-6c9a-b27c-dbfa-7c1de8d534f1.png" width="400px"/>

データベースが作成できたら、任意のデータを挿入

![スクリーンショット 2020-09-23 14.17.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/492c214c-2910-948e-b777-bb62cb4c1bd3.png)

コレクションはテーブルみたいなもので、ドキュメントはレコードみたいなものです。上記では、ドキュメントIDにランダム文字列を使用しています。

## Firestoreとアプリケーションの連携

まず、Reactアプリケーションに以下のSDKを導入

```bash
npm install firebase
```

次に `./src` に `firebase.ts` を作成し、以下の内容を記述

```typescript:firebase.ts
import firebase from "firebase/app";
import "firebase/firestore";
import { firebaseConfig } from "./firebaseConfig";

firebase.initializeApp(firebaseConfig);
export default firebase;
export const db = firebase.firestore();
```

`firebaseConfig.ts` の値は、webにてアプリを追加したのちに設定/全般ページの下の方で取得できます。

![スクリーンショット 2020-09-23 14.50.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/5fcfd452-ee9c-5773-f3c6-9c3b94b34e2d.png)

![スクリーンショット 2020-09-23 15.00.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/17bbba66-c34d-4ebc-dbf9-635efab391e6.png)

```typescript:firebaseConfig.ts
export const firebaseConfig = {
  apiKey: "",
  authDomain: "",
  databaseURL: "",
  projectId: "",
  storageBucket: "",
  messagingSenderId: "",
  appId: "",
};
```

**`firebaseConfig.ts` は認証情報が含まれるので、公開してはいけません。**`.gitignore` に含めておきましょう。

```text:.gitignore
...

# firebase
firebaseConfig.ts
```

## 一覧表示を実装

まず、連携が無事に出来ているかを確認

```tsx:App.tsx
import React, { useState } from "react";

type User = {
  id: string;
  name: string;
  height: number;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);

  // === 追記分 ===
  useEffect(() => {
    // 取得結果をコンソールに出力
    const usersRef = db.collection("users");
    usersRef.get().then((snapshot) => {
      snapshot.forEach((doc) => {
        console.log(doc.id, doc.data())
      });
    });
  }, []);
  // =============

  return (
    <div>
      <h2>Users</h2>
      {users.map((user) => (
        <div key={user.id}>
          {user.name} ({user.height}cm)
        </div>
      ))}
    </div>
  );
}

export default App;
```

ページを表示し、連携がうまくいっていたらデータがそのまま表示されるはずです。

![スクリーンショット 2020-09-23 19.45.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/0c7fbe64-cd4c-4c4e-bd0f-f8449e55894e.png)

取得したデータを `setUsers()` を利用して state に代入

```tsx:App.tsx
import React, { useEffect, useState } from "react";
import { db } from "./firebase";

type User = {
  id: string;
  name: string;
  height: number;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);

  // === 追記分 ===
  const fetchUsersData = () => {
    const usersRef = db.collection("users");
    usersRef.get().then((snapshot) => {
      const newUsers: any[] = [];
      snapshot.forEach((doc) => {
        newUsers.push({
          id: doc.id,
          ...doc.data(),
        });
      });
      setUsers(newUsers);
    });
  };

  useEffect(() => {
    fetchUsersData();
  }, []);
  // =============

  return (
    <div>
      <h2>Users</h2>
      {users.map((user) => (
        <div key={user.id}>
          {user.name} ({user.height}cm)
        </div>
      ))}
    </div>
  );
}

export default App;
```

![スクリーンショット 2020-09-23 20.04.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/86cc3c20-e919-b5f5-8d9e-1415241b2754.png)

## 追加、削除、更新の実装

### 削除処理

![スクリーンショット 2020-09-23 20.28.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/c6243ac5-351e-79df-51ad-abffdbd7f01d.png)

削除したタイミングでデータを取得し直すように実装しました。それと、一応のエラー処理も実装してみました。

```tsx:App.tsx
function App() {

  ...

  // 追加
  const handleDelete = (id: string) => {
    if (window.confirm("削除してもよろしいですか？")) {
      db.collection("users")
        .doc(id)
        .delete()
        .then(() => {
          fetchUsersData();
          alert("削除しました");
        })
        .catch(() => {
          alert("失敗しました");
        });
    }
  };

  ...

  return (
    <div>
      <h2>Users</h2>
      <table>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.height}cm</td>
              <td>
                <button onClick={() => handleDelete(user.id)}>削除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

...

```

### 追加処理

![スクリーンショット 2020-09-23 21.23.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/305522/d6e9c0ed-93fe-e76e-b54f-7dcbcf23ebea.png)


```tsx:App.tsx
function App() {

  ...

  // 追加
  const [addUserName, setAddUserName] = useState<string>("");
  const [addUserHeight, setAddUserHeight] = useState<number>(200);

  ...

  // 追加
  const handleAdd = () => {
    if (window.confirm("追加してもよろしいですか？")) {
      db.collection("users")
        .add({
          name: addUserName,
          height: addUserHeight,
        })
        .then(() => {
          fetchUsersData();
          setAddUserHeight(200);
          setAddUserName("");
          alert("追加しました");
        })
        .catch(() => {
          alert("失敗しました");
        });
    }
  };

  ...

  return (
    <div>
      <h2>Users</h2>
      {/* === 追加分 === */}
      <div>
        <label>
          NAME:{" "}
          <input
            type="text"
            value={addUserName}
            onChange={(event) => setAddUserName(event.target.value)}
          />
        </label>
        <label>
          HEIGHT:{" "}
          <input
            type="number"
            value={addUserHeight}
            onChange={(event) => setAddUserHeight(event.target.valueAsNumber)}
          />
        </label>
        <button onClick={() => handleAdd()}>追加</button>
      </div>
      {/* ============= */}
      <table>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td>{user.name}</td>
              <td>{user.height}cm</td>
              <td>
                <button onClick={() => handleDelete(user.id)}>削除</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

...

```

### 更新処理

実装は省略しますが、`set` メソッドで実装できます。

```typescript
db.collection('cities').doc('new-city-id').set(data);
```

## 最後に

短期間の開発ではこれでいいかもしれませんが、そうでない場合は共通化・モジュール化などしましょう！

## リンク

- [firestore SDKドキュメント](https://firebase.google.com/docs/firestore)
- [React ドキュメント](https://ja.reactjs.org/docs/getting-started.html)
- [CreateReactApp ドキュメント](https://create-react-app.dev/docs/documentation-intro)