【個人開発の記録】React.js+TypeScript+FirebaseでCRUDアプリ開発

# 背景

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
※この記事は現在執筆中です
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

ついに個人開発を始めることにした。
今回開発するWebアプリはCRUDアプリ。
Webページ上で動作し、ページを閉じてもデータを保持しておくことができる簡単なもの。

その開発の全手順をこの記事にまとめておくことにする。

# 開発するアプリについて

- CRUDアプリ
- Webページ上で動作する
- ページを閉じてもデータは保持しておくことができる

# 開発言語/フレームワーク

- フロントエンド：React.js + TypeScript
- バックエンド：Firebase

# 開発手順

## React+TypeScriptアプリの雛形を作成する

- ターミナルにて下記コマンドを実行して、React+TypeScriptアプリの雛形を作成する

```
npx create-react-app kashikojin1 --template typescript
```

- ターミナルにて、上記で作成したkashikojin1配下のpackage.josnのあるディレクトリで、下記コマンドを実行して、FirebaseのSDKとルーティング用のパッケージをインストールする

```
npm install firebase react-router-dom @types/react-router-dom
```

- React + TypeScriptのアプリを立ち上げてみる

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromeでアプリが起動する

```
npm run start
```

![スクリーンショット 2022-02-25 19.48.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/a245f291-c603-7934-783d-ea8aecf8002a.png)



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

- Firebaseのアカウントを作成する
- Firebaseのプロジェクトを作成する
- Authenticationメニューから認証機能を有効化する(参考:https://btj0.com/blog/react/firebase-auth/)
- ウェブアプリを追加する(参考:https://btj0.com/blog/react/firebase-auth/)
- Cloud Firestoreにてコレクションとドキュメントを作成する(参考:https://reffect.co.jp/react/react-crud-firebase-9#Cloud_Firestore)

コレクションとドキュメントはこんな感じ
（これはデータベースでいうテーブルとデータ）

![スクリーンショット 2022-02-26 10.33.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/4a214d01-3543-e297-ffdf-5ed6902e4558.png)

![スクリーンショット 2022-02-26 10.33.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/fe313e76-b748-2a36-2ce4-d9ec0b768f79.png)




## React+TypeScript+Firebaseをいろいろ触ってみた(やらなくていい)

### １．データ(コレクションの中身の全ドキュメント)を表示する

先程作成したデータ(ドキュメント)を画面に表示してみる

プロジェクトのフォルダ構成を下記のようにする
★マークのファイルはこれから新規作成するファイル
☆マークのファイルはこれから編集するファイル

```
kashikojin1(プロジェクト名)
  └┬ build
   ┝ node_modules
   ┝ public
   ┝ src
   |   └┬ App.css
   |    ┝ App.tsx ☆
   |    ┝ firebase.tsx ★
   |    ┝ index.css
   |    ┝ index.js
   |    └ api
   |        └ hello.js
   ┝ .gitignore
   ┝ package-lock.json
   ┝ package.json
   ┝ README.md
   ┝ tsconfig.json
   └ .env ★
```

では★で示したファイルを作成していこう

```.env(新規作成)
REACT_APP_FIREBASE_KEY="Firebaseで作成したウェブアプリのapiKey"
REACT_APP_FIREBASE_DOMAIN="Firebaseで作成したウェブアプリのauthDomain"
REACT_APP_FIREBASE_PROJECT_ID="Firebaseで作成したウェブアプリのprojectId"
REACT_APP_FIREBASE_STORAGE_BUCKET="Firebaseで作成したウェブアプリのstorageBucket"
REACT_APP_FIREBASE_SENDER_ID="Firebaseで作成したウェブアプリのmessagingSenderId"
REACT_APP_FIREBASE_APP_ID="Firebaseで作成したウェブアプリのappId"
REACT_APP_FIREBASE_MEASUREMENT_ID="Firebaseで作成したウェブアプリのmeasurementId"
```

```typescript:kashikojin1/src/firebase.tsx(新規作成)
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_KEY,
  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,
  measurementId: process.env.REACT_APP_FIREBASE_MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig)

export const db = getFirestore();
export const storage = getStorage();

export default app;
```

```typescript:kashikojin1/src/App.tsx(編集)
import { db } from '../src/firebase';
import { useState, useEffect }  from 'react';
import { collection, getDocs, QuerySnapshot } from 'firebase/firestore';
import './App.css';

type User = {
  name: string;
  email: string;
  age: number;
  admin: boolean;
};

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

  useEffect(() => {
    const usersCollectionRef = collection(db, 'users');
    getDocs(usersCollectionRef).then((querySnapshot) => {
      const  userList: User[] = [];
      let count: number = 0;
      querySnapshot.docs.map((doc, index) => {
        if (count === index ) {
          const user: User= {
            name: doc.data().name,
            email: doc.data().email,
            age: doc.data().age,
            admin: doc.data().admin,
          };
          userList.push(user);
          count += 1;
        };
      });
      setUsers(userList);
    });
  }, []);

  return (
    <div>
      {users.map((user, index) => (
        <div key={index.toString()}>{user.name}</div>
      ))}
    </div>
  );
}

export default App;
```

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromeでアプリが起動する

```
npm run start
```

画面にコレクション1件(ドキュメント2件分)の文字列が出力することを確認できる

### ２．データ(指定したドキュメント)を表示する

App.tsxの中身を少し変えてみる

```typescript:kashikojin1/src/App.tsx(編集)
import { db } from '../src/firebase';
import { useState, useEffect }  from 'react';
import { doc, getDoc } from 'firebase/firestore';
import './App.css';

type User = {
  name: string;
  email: string;
  age: number;
  admin: boolean;
};

function App() {
  useEffect(() => {
    const usersCollectionRef = doc(db, 'users', 'lNQjamODq8TnRoJcvBFC');
    getDoc(usersCollectionRef).then((documentSnapshot) => {
      if (documentSnapshot.exists()) {
        console.log(documentSnapshot.data());
      } else {
        console.log('No such document!');
      }
    });
  }, []);

  return (
    <div>
    </div>
  );
}

export default App;
```

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromeでアプリが起動する

```
npm run start
```

コンソールにドキュメント1件分の内容が出力することを確認できる

### ３．Cloud Firestoreにてデータ(ドキュメント)を追加したときリアルタイムでデータを出力する

App.tsxの中身を少し変えてみる


```typescript:kashikojin1/src/App.tsx(編集)
import { db } from '../src/firebase';
import { useEffect }  from 'react';
import { collection, onSnapshot } from 'firebase/firestore';
import './App.css';

type User = {
  name: string;
  email: string;
  age: number;
  admin: boolean;
};

function App() {

  useEffect(() => {
    const usersCollectionRef = collection(db, 'users');
    const unsub = onSnapshot(usersCollectionRef, (querySnapshot) => {
      querySnapshot.docs.map((doc) => {
        console.log(doc.data());
      });
    });
    return unsub;
  }, []);

  return (
    <div>
    </div>
  );
}

export default App;
```

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromeでアプリが起動する

```
npm run start
```

Cloud Firestoreにてデータ(ドキュメント)を追加すると、ドキュメントがコンソールに出力することを確認できる


### ４．React+TypeScriptアプリの画面上からデータを追加する

App.tsxの中身を少し変えてみる

```typescript:kashikojin1/src/App.tsx(編集)
import { useState,useEffect }  from 'react';
import { db } from '../src/firebase';
import { addDoc, collection, onSnapshot } from 'firebase/firestore';
import { useForm, SubmitHandler } from 'react-hook-form';
import './App.css';

type User = {
  name: string;
  email: string;
  age: number;
  admin: boolean;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const { register,
          handleSubmit,
          watch,
          formState: { errors }
        } = useForm<User>();

  const onSubmit: SubmitHandler<User> = (data) => {
    console.log('onSubmit', data);
    const usersCollectionRef = collection(db, 'users');
    const documentRef = addDoc(usersCollectionRef, {
      name: data.name,
      email: data.email,
      age: data.age,
      admin: false,
    });
    console.log(documentRef);
  };
  
  useEffect(() => {
    let userList: User[] = [];
    const usersCollectionRef = collection(db, 'users');
    const unsub = onSnapshot(usersCollectionRef, (querySnapshot) => {
      querySnapshot.docs.map((doc) => {
        const user: User= {
          name: doc.data().name,
          email: doc.data().email,
          age: doc.data().age,
          admin: doc.data().admin,
        };
        userList.push(user);
      });
      setUsers(userList);
    });
    return unsub;
  });

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>名前</label>
          <input defaultValue="test" {...register('name')} />
        </div>
        <div>
          <label>年齢</label>
          <input defaultValue="test" {...register('age')} />
        </div>
        <div>
          <label>メールアドレス</label>
          <input defaultValue="test" {...register('email')} />
        </div>
        {errors.name && (
          <span>Error!!!</span>
        )}
        <input value="登録" type="submit" />
      </form>
      <div>
        {users.map((user, index) => (
          <div key={index.toString()}>{user.name}</div>
        ))}
      </div>
    </div>
  );
}

export default App;
```

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromでアプリが起動する

```
npm run start
```

入力フォームに文字列を入力して登録ボタンを押下すると、Cloud FIrestoreへデータが登録され、登録されているデータが画面に出力されることを確認できる


### ５．React+TypeScriptアプリの画面上からデータを削除する

App.tsxの中身を少し変えてみる

```typescript:kashikojin1/src/App.tsx(編集)
import { useState,useEffect }  from 'react';
import { db } from '../src/firebase';
import { addDoc, collection, onSnapshot } from 'firebase/firestore';
import { useForm, SubmitHandler } from 'react-hook-form';
import './App.css';

type User = {
  name: string;
  email: string;
  age: number;
  admin: boolean;
};

function App() {
  const [users, setUsers] = useState<User[]>([]);
  const { register,
          handleSubmit,
          watch,
          formState: { errors }
        } = useForm<User>();

  const onSubmit: SubmitHandler<User> = (data) => {
    console.log('onSubmit', data);
    const usersCollectionRef = collection(db, 'users');
    const documentRef = addDoc(usersCollectionRef, {
      name: data.name,
      email: data.email,
      age: data.age,
      admin: false,
    });
    console.log(documentRef);
  };
  
  useEffect(() => {
    let userList: User[] = [];
    const usersCollectionRef = collection(db, 'users');
    const unsub = onSnapshot(usersCollectionRef, (querySnapshot) => {
      querySnapshot.docs.map((doc) => {
        const user: User= {
          name: doc.data().name,
          email: doc.data().email,
          age: doc.data().age,
          admin: doc.data().admin,
        };
        userList.push(user);
      });
      setUsers(userList);
    });
    return unsub;
  });

  return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <div>
          <label>名前</label>
          <input defaultValue="test" {...register('name')} />
        </div>
        <div>
          <label>年齢</label>
          <input defaultValue="test" {...register('age')} />
        </div>
        <div>
          <label>メールアドレス</label>
          <input defaultValue="test" {...register('email')} />
        </div>
        {errors.name && (
          <span>Error!!!</span>
        )}
        <input value="登録" type="submit" />
      </form>
      <div>
        {users.map((user, index) => (
          <div key={index.toString()}>{user.name}</div>
        ))}
      </div>
    </div>
  );
}

export default App;
```

ターミナルにて、package.josnのあるディレクトリで、下記コマンドを実行するとChromでアプリが起動する

```
npm run start
```

一覧の右側にある削除ボタンを押下すると、ドキュメントIDをキーにCloud FIrestoreからデータを削除する






# 参考

- [React環境でFirebase SDK v9のFirestoreでのCRUDの操作を学ぶ](https://reffect.co.jp/react/react-crud-firebase-9)
- [【完全版】ReactのFirebase Authentication(認証)を基礎からマスターする](https://reffect.co.jp/react/react-firebase-auth)
- [Reactで手っ取り早くタブを実現する方法【react-tabs】](https://qiita.com/laughingman/items/cab91e7dabc952247953)