
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」の認証を有効にします
※細かい手順は省略します

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


- 以下の記事の「Cloud Firestoreのデータベースの作成」を参考に、Cloud Firestoreにてコレクションとドキュメントを作成します
実装
まずはFirebaseの「Firestore Database」にてコレクション(データベースでいうテーブル)とドキュメント(データベースでいうレコード)を作成します
- コレクション:「tTasks」という名称で作成します
- ドキュメント:ドキュメントIDは適当でOKです
「taskText(string型)」「timeStamp(string型)」を持ったドキュメントを1件作成します

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から削除する

コメント