React.js+TypeScript+FirebaseでToDoアプリを作ってみた

# 背景

Firebaseに慣れるために簡単なToDoアプリを作ってみたのでアウトプットしておくことにする

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

- ToDoアプリ
- CRUD機能は「表示」「追加」「削除」のみ
- チェックを入れるとテキスト色が赤になる
- 見た目は特に装飾していないので各自CSSでカスタマイズすること

![スクリーンショット 2022-03-09 11.49.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/a9ef95bc-8a81-9ad3-90fe-a78439177d40.png)


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

- フロントエンド
  - React.js
  - TypeScript


- バックエンド
  - Firebase
  - Node.js

※ちなみに今回はFirebaseの無料プランを選択

# 開発手順

## 準備：Node.jsのインストール(まだNode.jsをインストールしていない方のみ)

こちらからNode.jsをインストールする

https://nodejs.org/en/

インストールするとnpm、npxコマンドが使用できるようになる

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

- ターミナルにて下記コマンドを実行して、React+TypeScriptアプリの雛形を作成する
※プロジェクト名が「kashikojin1」 の場合

```
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
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/28d86238-c8f2-f784-dd12-8d47d09df4de.png)

## 準備：Reactライブラリのインストール


- Material-UIのインストール

```
npm install @mui/material material-ui/core
```

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

- Firebaseのアカウントを作成する
Firebase公式HPからアカウントを作成する
https://firebase.google.com

- Firebaseのプロジェクトを作成する
赤で囲った部分をクリックしてプロジェクトを作成する
※細かい手順は省略

![スクリーンショット 2022-03-08 23.13.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/89aafa3a-1e56-5fc2-bbbd-efe7a64b68c4.png)

- Authenticationメニューから認証機能を有効化する(不要かも)
作成したプロジェクトの「Authentication」メニューから「Sign-in method」タブを選択して「メール/パスワード」と「Google」の認証を有効にする
※細かい手順は省略

![スクリーンショット 2022-03-08 23.28.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/be13b322-1fbe-4016-d603-208e2fe8e7d9.png)

- ウェブアプリを追加する
プロジェクトのホーム画面で「アプリを追加」ボタンをクリックしてWebアプリを追加する。アプリを追加するとFirebaseに接続するためのAPIキー等の情報を取得できる。これらの情報は次に作成するReactアプリで利用する

![スクリーンショット 2022-03-09 0.08.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/e40fadd0-3c4e-a6e5-eb8e-62bf1de03157.png)


![スクリーンショット 2022-03-09 12.25.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/67b6fa9b-a559-78d7-8566-5581d921967b.png)

- Cloud Firestoreにてコレクションとドキュメントを作成する
この記事( https://reffect.co.jp/react/react-crud-firebase-9#Cloud_Firestore )の「Cloud Firestoreのデータベースの作成」を参考にしながらデータベースを作成する


## 実装

まずはFirebaseの「Firestore Database」にてコレクション(データベースでいうテーブル)とドキュメント(データベースでいうレコード)を作成する

- コレクション：「tTasks」という名称で作成
- ドキュメント：ドキュメントIDは適当でOK
「taskText(string型)」「timeStamp(string型)」を持ったドキュメントを1件作成する



![スクリーンショット 2022-03-09 11.56.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/ccdf9e37-92d0-f09f-93df-76a5afa4190c.png)

React.js+TypeScriptアプリへ下記を追加する

```typescript:src/App.tsx
import './App.css';
import TaskManagement from './components/TaskManagement'

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

export default App;
```

```typescript:src/components/TaskManagement.tsx
import { useState, useEffect }  from 'react';
import { db } from '../../firebase';
import CommonDialog from '../CommonDialog';
import { doc, getDocs, addDoc, collection, deleteDoc } from 'firebase/firestore';
import { Button, TextField, Checkbox } from '@mui/material'
import {
  Typography,
  TableContainer,
  Table,
  TableHead,
  TableBody,
  TableRow,
  TableCell,
  makeStyles
} from '@material-ui/core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'

const useStyle = makeStyles((theme) => ({
  taskTime: {
    fontSize: '8px',
  },
}));

type Task = {
  docId: string;
  taskText: string;
  timeStamp: string;
};

function TaskManagement() {
  const classes = useStyle();
  const [taskList, setTaskList] = useState<Task[]>([]);
  const [taskText, setTaskText] = useState<string>('');
  const [isOpenDeleteConfirm, setIsOpenDeleteConfirm] = useState(false);
  const [deleteDocId, setDeleteDocId] = useState<string>('');

  // 表示
  const dispData = () => {
    const tasksCollectionRef = collection(db, 'tTasks');
    getDocs(tasksCollectionRef).then((querySnapshot) => {
      const  userList: Task[] = [];
      let count: number = 0;
      querySnapshot.docs.map((doc, index) => {
        const task: Task = {
          docId: doc.id,
          taskText: doc.data().taskText,
          timeStamp: doc.data({serverTimestamps:"estimate"}).timeStamp,
        };
        userList.push(task);
        count += 1;
      });
      setTaskList(userList);
    });
  };

  // 登録
  const addTask = (inputText: string) => {
    if (inputText === '') {
      return;
    };
    const tasksCollectionRef = collection(db, 'tTasks');
    const nowTime = new Date();
    const nowYear = nowTime.getFullYear();
    const nowMonth = nowTime.getMonth();
    const nowDay = nowTime.getDate();
    const nowHour = nowTime.getHours();
    const nowMin = nowTime.getMinutes();
    const nowSec = nowTime.getSeconds();
    const documentRef = addDoc(tasksCollectionRef, {
      taskText: inputText,
      timeStamp: `${nowYear}/${nowMonth}/${nowDay} ${nowHour}:${nowMin}:${nowSec}`,
    });
    setTaskText('');
    dispData();
  };

  // 削除(確認)
  const deleteTaskConfirm = (docId: string) => {
    setDeleteDocId(docId);
    setIsOpenDeleteConfirm(true);
  };

  // 削除
  const deleteTask = async() => {
    setIsOpenDeleteConfirm(false);
    const userDocumentRef = doc(db, 'tTasks', deleteDocId);
    await deleteDoc(userDocumentRef);
    dispData();
  };

  // タスクチェックボックスのオンオフ切り替え時
  const changeTaskChecked = (blnChecked: boolean, numIndex: number) => {
    // オフ→オンのときテキストの文字色を変える
    if (blnChecked === true) {
      const taskText = document.getElementById(`taskText${numIndex}`);
      if (taskText !== null) {
        taskText.style.color = '#FF0000';
      };
    } else {
      const taskText = document.getElementById(`taskText${numIndex}`);
      if (taskText !== null) {
        taskText.style.color = '#000000';
      };
    };
  };

  // 初期処理
  useEffect(() => {
    dispData();
  }, []);

  return (
    <>
      <TableContainer>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>
              </TableCell>
              <TableCell>
              </TableCell>
              <TableCell>
              </TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {taskList.map((user, index) => (
              <>
              <TableRow key={index.toString()}>
                <TableCell>
                  <Checkbox
                    onChange={(e) => changeTaskChecked(e.target.checked, index)}  
                  />
                </TableCell>
                <TableCell>
                  <Typography id={`taskText${index.toString()}`}>
                    {user.taskText}
                  </Typography>
                  <Typography className={classes.taskTime}>
                    {user.timeStamp.toString()}
                  </Typography>
                </TableCell>
                <TableCell>
                  <Button
                    variant="outlined"
                    color="error"
                    onClick={() => deleteTaskConfirm(user.docId)}
                  >
                    <FontAwesomeIcon
                      icon={faTrashAlt}
                      fixedWidth
                    />
                  </Button>
                </TableCell>
              </TableRow>
              <CommonDialog
                msg="このタスクを削除しますか？"
                isOpen={isOpenDeleteConfirm}
                doYes={deleteTask}
                doNo={() => {setIsOpenDeleteConfirm(false)}}
              />
              </>
            ))}
            <TableRow>
              <TableCell>
              </TableCell>
              <TableCell>
                <TextField
                  value={taskText}
                  label="Todoを入力"
                  variant="standard" 
                  size="small"
                  fullWidth
                  onChange={(e) => {setTaskText(e.target.value)}}
                />
              </TableCell>
              <TableCell>
                <Button
                  variant="outlined"
                  onClick={() => addTask(taskText)}
                >
                  ＋
                </Button>
              </TableCell>
            </TableRow>
          </TableBody>
        </Table>
      </TableContainer>
    </>
  );
}

export default TaskManagement;
```

```css:src/App.css
.App {
  text-align: center;
}

.App-logo {
  height: 40vmin;
  pointer-events: none;
}

@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
```
上記の準備フェーズで取得したAPIキー等の情報をここで入力する

```typescript:src/firebase.tsx
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: "xxxxx",
  authDomain: "xxxxx",
  projectId: "xxxxx",
  storageBucket: "xxxxx",
  messagingSenderId: "xxxxx",
  appId: "xxxxx",
  measurementId: "xxxxx"
};

const app = initializeApp(firebaseConfig)

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

export default app;
```


# 実行

ターミナルを起動して、package.jsonのあるディレクトリで下記コマンドを実行する

```
npm run start
```

## アプリを動かしてみる

- 「Todoを入力」というテキストボックスへ適当な文字列を入力して「＋」ボタンを押下するとToDoへ追加することができる
- ToDoの左側のチェックボックスにチェックを入れるとテキスト色が赤になる
- 「ゴミ箱」ボタンを押下するとToDoから削除する


![スクリーンショット 2022-03-09 11.49.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/758310/a9ef95bc-8a81-9ad3-90fe-a78439177d40.png)



# 参考
- https://btj0.com/blog/react/firebase-auth/
- https://reffect.co.jp/react/react-crud-firebase-9

不明点があればお気軽にメッセージやTwitterDMでご連絡ください