背景
Firebaseに慣れるために簡単なToDoアプリを作ってみたのでアウトプットしておくことにする
開発するアプリについて
- ToDoアプリ
- CRUD機能は「表示」「追加」「削除」のみ
- チェックを入れるとテキスト色が赤になる
- 見た目は特に装飾していないので各自CSSでカスタマイズすること
開発言語/フレームワーク
-
フロントエンド
- 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
準備:Reactライブラリのインストール
- Material-UIのインストール
npm install @mui/material material-ui/core
準備:Firebaseのプロジェクトを作成する
-
Firebaseのアカウントを作成する
Firebase公式HPからアカウントを作成する
https://firebase.google.com -
Firebaseのプロジェクトを作成する
赤で囲った部分をクリックしてプロジェクトを作成する
※細かい手順は省略
- Authenticationメニューから認証機能を有効化する(不要かも)
作成したプロジェクトの「Authentication」メニューから「Sign-in method」タブを選択して「メール/パスワード」と「Google」の認証を有効にする
※細かい手順は省略
- ウェブアプリを追加する
プロジェクトのホーム画面で「アプリを追加」ボタンをクリックしてWebアプリを追加する。アプリを追加するとFirebaseに接続するためのAPIキー等の情報を取得できる。これらの情報は次に作成するReactアプリで利用する
- 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件作成する
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から削除する
参考
不明点があればお気軽にメッセージやTwitterDMでご連絡ください