React.js+TypeScript+FirebaseでToDoアプリを作成する手順

React

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

今回はそんな要望へ回答します。

この記事を読んでいただければ、React.js+TypeScript+FirebaseでToDoアプリを作成することができます。

開発するアプリについて

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

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

  • フロントエンド
    • React.js
    • TypeScript
  • バックエンド
    • Firebase
    • Node.js

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

開発手順

準備:Node.jsのインストール

こちらからNode.jsをインストールします。※まだインストールしていない場合のみ

インストールすると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

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

  • Material-UIのインストール
npm install @mui/material material-ui/core

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

  • Firebaseのアカウントを作成します
    Firebase公式HPからアカウントを作成します
  • Firebaseのプロジェクトを作成します
    赤で囲った部分をクリックしてプロジェクトを作成します
    ※細かい手順は省略します
  • Authenticationメニューから認証機能を有効化します(不要かも)
    作成したプロジェクトの「Authentication」メニューから「Sign-in method」タブを選択して「メール/パスワード」と「Google」の認証を有効にします
    ※細かい手順は省略します
スクリーンショット 2022-03-08 23.28.44.png
  • ウェブアプリを追加します
    プロジェクトのホーム画面で「アプリを追加」ボタンをクリックしてWebアプリを追加する。アプリを追加するとFirebaseに接続するためのAPIキー等の情報を取得できます。これらの情報は次に作成するReactアプリで利用します
スクリーンショット 2022-03-09 0.08.13.png
スクリーンショット 2022-03-09 12.25.54.png
  • 以下の記事の「Cloud Firestoreのデータベースの作成」を参考に、Cloud Firestoreにてコレクションとドキュメントを作成します

実装

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

  • コレクション:「tTasks」という名称で作成します
  • ドキュメント:ドキュメントIDは適当でOKです
    「taskText(string型)」「timeStamp(string型)」を持ったドキュメントを1件作成します
スクリーンショット 2022-03-09 11.56.44.png

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

import './App.css';
import TaskManagement from './components/TaskManagement';

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

export default App;

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;

.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キー等の情報をここで入力する

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

コメント

タイトルとURLをコピーしました