LoginSignup
10
6

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-09

背景

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

開発するアプリについて

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

スクリーンショット 2022-03-09 11.49.47.png

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

  • フロントエンド

    • React.js
    • TypeScript
  • バックエンド

    • Firebase
    • Node.js

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

開発手順

準備:Node.jsのインストール(まだ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

image.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

  • 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

実装

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

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

スクリーンショット 2022-03-09 11.56.44.png

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

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

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

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

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

参考

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

10
6
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6