1. 概要
前回はLocal Storageの使い方についてでした。今回はFirestoreの使い方についてです。
対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。
2. nodeのインストール
こちらを参考
3. プロジェクトを作成
こちらを参考
4. 必要なライブラリをインストール
こちらを参考
npm install firebase
5. ソースコード
※前回より差分のみを記載
5-1-1. .env.local
DB_HOST=localhost
DB_USER=user
DB_PASSWORD=password
DB_NAME=test
NEXT_PUBLIC_FIREBASE_API_KEY=[★]
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=[★]
NEXT_PUBLIC_FIREBASE_PROJECT_ID=[★]
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=[★]
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=[★]
NEXT_PUBLIC_FIREBASE_APP_ID=[★]
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=[★]
5-1-2. src/lib/firebase/firebase.ts
import { initializeApp, FirebaseApp } from "firebase/app";
import { Firestore, getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};
const app: FirebaseApp = initializeApp(firebaseConfig);
const firestore: Firestore = getFirestore(app);
export { firestore };
5-1-3. src/lib/firebase/firestore-data-converter.ts
import {
DocumentData,
FirestoreDataConverter,
QueryDocumentSnapshot,
SnapshotOptions,
WithFieldValue,
} from "firebase/firestore";
const CreateFirestoreDataConverter = <
T extends DocumentData
>(): FirestoreDataConverter<T> => {
return {
toFirestore(data: WithFieldValue<T>): DocumentData {
return data;
},
fromFirestore(
snapshot: QueryDocumentSnapshot<T>,
options: SnapshotOptions
): T {
return snapshot.data(options);
},
};
};
export default CreateFirestoreDataConverter;
5-1-4. src/lib/firebase/firestore-helper.ts
import {
Firestore,
collection,
doc,
CollectionReference,
DocumentData,
DocumentReference,
getDocs,
getDoc,
DocumentSnapshot,
QuerySnapshot,
setDoc,
addDoc,
deleteDoc,
updateDoc,
} from "firebase/firestore";
import { firestore } from "@/lib/firebase/firebase";
import CreateFirestoreDataConverter from "./firestore-data-converter";
const getCollectionRef = <T extends DocumentData>(
db: Firestore,
path: string,
pathSegments?: string[]
): CollectionReference<T, DocumentData> => {
if (pathSegments != undefined) {
return collection(db, path, ...pathSegments).withConverter(
CreateFirestoreDataConverter<T>()
);
} else {
return collection(db, path).withConverter(
CreateFirestoreDataConverter<T>()
);
}
};
const getDocRef = <T extends DocumentData>(
db: Firestore,
path: string,
pathSegments: string[]
): DocumentReference<T, DocumentData> => {
return doc(db, path, ...pathSegments).withConverter(
CreateFirestoreDataConverter<T>()
);
};
const getCollectionData = async <T extends DocumentData>(
path: string,
pathSegments?: string[]
): Promise<T[]> => {
const collectionRef: CollectionReference<T, DocumentData> =
getCollectionRef<T>(firestore, path, pathSegments);
const collection: Promise<QuerySnapshot<T, DocumentData>> =
getDocs(collectionRef);
const data: T[] = [];
(await collection).docs.map((doc) => {
data.push(doc.data());
});
return data;
};
const getDocumentData = async <T extends DocumentData>(
path: string,
pathSegments: string[]
): Promise<T | undefined> => {
const docRef: DocumentReference<T, DocumentData> = getDocRef<T>(
firestore,
path,
pathSegments
);
const doc: Promise<DocumentSnapshot<T, DocumentData>> = getDoc(docRef);
return (await doc).data();
};
const addCollectioin = async <T extends DocumentData>(
path: string,
pathSegments: string[],
params: T
): Promise<DocumentReference<T, DocumentData>> => {
const collectioinRef: CollectionReference<T, DocumentData> =
getCollectionRef<T>(firestore, path, pathSegments);
return await addDoc(collectioinRef, { ...params });
};
const addDocument = async <T extends DocumentData>(
path: string,
pathSegments: string[],
params: T
): Promise<void> => {
const docRef: DocumentReference<T, DocumentData> = getDocRef<T>(
firestore,
path,
pathSegments
);
await setDoc(docRef, { ...params });
};
const deleteDocument = async <T extends DocumentData>(
path: string,
pathSegments: string[]
): Promise<void> => {
const docRef: DocumentReference<T, DocumentData> = getDocRef<T>(
firestore,
path,
pathSegments
);
await deleteDoc(docRef);
};
const updateDocument = async <T extends DocumentData>(
path: string,
pathSegments: string[],
params: T
): Promise<void> => {
const docRef: DocumentReference<T, DocumentData> = getDocRef<T>(
firestore,
path,
pathSegments
);
await updateDoc(docRef, { ...params });
};
export {
getCollectionData,
getDocumentData,
addCollectioin,
addDocument,
deleteDocument,
updateDocument,
};
5-1-5. src/lib/components/msg-snackbar.tsx
import React from "react";
import { Snackbar, Alert, Slide, AlertColor } from "@mui/material";
import ClearIcon from "@mui/icons-material/Clear";
interface Props {
open: boolean;
severity: AlertColor;
message: string;
handleClick: any;
}
const MessageSnackbar = (props: Props) => {
const { open, severity, message, handleClick } = props;
return (
<Snackbar
open={open}
autoHideDuration={3000}
transitionDuration={{ enter: 1000, exit: 2000 }}
TransitionComponent={Slide}
TransitionProps={{ enter: true, exit: false }}
sx={{ height: "10%" }}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
onClose={() =>
handleClick({
open: false,
severity,
})
}
>
<Alert
severity={severity}
action={
<ClearIcon
onClick={() =>
handleClick({
open: false,
severity,
})
}
/>
}
>
{message}
</Alert>
</Snackbar>
);
};
export default MessageSnackbar;
5-1-6. src/app/components/component07/model.ts
import { FieldValue } from "firebase/firestore";
export type User = {
userId: string;
name: string;
createdAt: FieldValue;
};
export type Booking = {
place: string;
price: number;
createdAt: FieldValue;
};
export const COLLECTION_NAME_USERS = "users";
export const COLLECTION_NAME_USERID = "test";
export const SUB_COLLECTION_NAME_BOOKINGS = "bookings";
5-1-7. src/app/components/component07/regist-data.tsx
"use client";
import { serverTimestamp } from "firebase/firestore";
import { addDocument } from "@/lib/firebase/firestore-helper";
import { Button, Stack, TextField } from "@mui/material";
import { ChangeEvent, useState } from "react";
import {
Booking,
COLLECTION_NAME_USERID,
COLLECTION_NAME_USERS,
SUB_COLLECTION_NAME_BOOKINGS,
} from "./model";
const RegistData = () => {
const [place, setPlace] = useState<string>("");
const [price, setPrice] = useState<number>(0);
const handleAdd = async () => {
await addDocument<Booking>(
COLLECTION_NAME_USERS,
[COLLECTION_NAME_USERID, SUB_COLLECTION_NAME_BOOKINGS, place],
{ place, price, createdAt: serverTimestamp() }
);
setPlace("");
setPrice(0);
};
const handlePlace = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
e.preventDefault();
setPlace(e.currentTarget.value);
};
const handlePrice = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
e.preventDefault();
setPrice(Number(e.currentTarget.value));
};
return (
<Stack direction="row" spacing={1} sx={{ width: "100%" }}>
<TextField
size="small"
label="Place"
value={place}
onChange={(e) => handlePlace(e)}
/>
<TextField
size="small"
label="Price"
type="number"
InputLabelProps={{ shrink: true }}
value={price}
onChange={(e) => handlePrice(e)}
/>
<Button
variant="contained"
size="medium"
color="primary"
onClick={() => handleAdd()}
>
Add
</Button>
</Stack>
);
};
export default RegistData;
5-1-8. src/app/components/component07/display-data.tsx
"use client";
import { useEffect, useState } from "react";
import { AlertColor, Button } from "@mui/material";
import scss from "./page.module.scss";
import {
getDocumentData,
getCollectionData,
deleteDocument,
} from "@/lib/firebase/firestore-helper";
import {
User,
Booking,
COLLECTION_NAME_USERS,
COLLECTION_NAME_USERID,
SUB_COLLECTION_NAME_BOOKINGS,
} from "./model";
import MessageSnackbar from "@/lib/components/msg-snackbar";
interface SnackbarState {
open: boolean;
severity: AlertColor;
}
const DisplayData = () => {
const [data, setData] = useState<Booking[]>();
useEffect(() => {
const getData = async () => {
const data: Booking[] = await getCollectionData<Booking>(
COLLECTION_NAME_USERS,
[COLLECTION_NAME_USERID, SUB_COLLECTION_NAME_BOOKINGS]
);
setData(data);
};
getData();
}, [data]);
const handleDelete = async (booking: Booking) => {
await deleteDocument<Booking>(COLLECTION_NAME_USERS, [
COLLECTION_NAME_USERID,
SUB_COLLECTION_NAME_BOOKINGS,
booking.place,
]);
};
const [msg, setMsg] = useState<string>("");
const [state, setState] = useState<SnackbarState>({
open: false,
severity: "success",
});
const { open, severity } = state;
const handleClick = (booking: Booking, newState: SnackbarState) => {
setMsg(`${booking.place}:${booking.price}`);
setState({ ...newState });
};
return (
<div>
<table>
<thead>
<tr>
<th>Place</th>
<th>Price</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{data?.map((booking: Booking, i: number) => (
<tr key={i}>
<td
onClick={() =>
handleClick(booking, {
open: true,
severity: "success",
})
}
>
{booking.place}
</td>
<td
className={scss.td_right}
onClick={() =>
handleClick(booking, {
open: true,
severity: "success",
})
}
>
{booking.price}
</td>
<td className={scss.td_center}>
<Button
variant="contained"
size="small"
color="primary"
onClick={() => handleDelete(booking)}
>
Delete
</Button>
</td>
</tr>
))}
</tbody>
</table>
{severity === "success" ? (
<MessageSnackbar
open={open}
severity="success"
message={msg}
handleClick={handleClick}
/>
) : (
<></>
)}
</div>
);
};
export default DisplayData;
5-1-9. src/app/components/component07/page.module.scss
.component {
color: blue;
& ul {
margin-left: 20px;
& li {
list-style: disc;
}
}
& table {
border: 1px solid;
width: 80%;
& tr,
td {
border: 1px solid;
padding: 5px;
cursor: pointer;
}
& th {
border: 1px solid;
padding: 5px;
text-align: center;
background-color: lavender;
}
& .td_right {
text-align: right;
}
& .td_center {
text-align: center;
}
}
}
5-1-10. src/app/components/component07/page.tsx
import GoBack from "@/lib/components/go-back";
import scss from "./page.module.scss";
import RegistData from "./regist-data";
import DisplayData from "./display-data";
const Component07 = () => {
return (
<div className={scss.component}>
<GoBack />
<br />
<br />
<ul>
<li>Firestore</li>
</ul>
<RegistData />
<br />
<DisplayData />
</div>
);
};
export default Component07;
5-1-11. src/app/components/page.tsx
"use client";
import React from "react";
import { Link } from "@mui/material";
import scss from "./page.module.scss";
const Components = () => {
return (
<div className={scss.components}>
<ul>
<li>
<Link href="/components/component01" underline="hover">
Component01
</Link>
</li>
<li>
<Link href="/components/component02" underline="hover">
Component02
</Link>
</li>
<li>
<Link href="/components/component03" underline="hover">
Component03
</Link>
</li>
<li>
<Link href="/components/component04" underline="hover">
Component04
</Link>
</li>
<li>
<Link href="/components/component05" underline="hover">
Component05
</Link>
</li>
<li>
<Link href="/components/component06" underline="hover">
Component06
</Link>
</li>
<li>
<Link href="/components/component07" underline="hover">
Component07
</Link>
</li>
</ul>
</div>
);
};
export default Components;
6. サーバーを起動
npm run dev
7. ブラウザで確認
- http://localhost:3000
8. ディレクトリの構造
.
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── js
│ │ └── script.js
│ ├── next.svg
│ └── vercel.svg
├── src
│ ├── app
│ │ ├── components
│ │ │ ├── component01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component02
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component03
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component04
│ │ │ │ ├── checkbox-demo.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ ├── radio-demo.tsx
│ │ │ │ └── select-demo.tsx
│ │ │ ├── component05
│ │ │ │ ├── canvas.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component06
│ │ │ │ ├── my-local-storage.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── component07
│ │ │ │ ├── display-data.tsx
│ │ │ │ ├── model.ts
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ └── regist-data.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── events
│ │ │ ├── event01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── globals.scss
│ │ ├── hooks
│ │ │ ├── hook01
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook02
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook03
│ │ │ │ ├── child.tsx
│ │ │ │ ├── counter-provider.tsx
│ │ │ │ ├── grandchild.tsx
│ │ │ │ ├── myself.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook04
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook05
│ │ │ │ ├── child.tsx
│ │ │ │ ├── counter-provider.tsx
│ │ │ │ ├── grandchild.tsx
│ │ │ │ ├── myself.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── hook06
│ │ │ │ ├── child.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ ├── play-provider.tsx
│ │ │ │ ├── text-box.tsx
│ │ │ │ └── video-player.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── layout.module.scss
│ │ ├── layout.tsx
│ │ ├── nextjs
│ │ │ ├── nextjs01
│ │ │ │ ├── child
│ │ │ │ │ ├── client-page.tsx
│ │ │ │ │ ├── metadata.ts
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── nextjs02
│ │ │ │ ├── [...slug]
│ │ │ │ │ └── page.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ └── shop
│ │ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ │ ├── nextjs03
│ │ │ │ ├── fetch-image.tsx
│ │ │ │ ├── get-image.tsx
│ │ │ │ ├── loading.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── nextjs04
│ │ │ │ ├── actions.ts
│ │ │ │ ├── list.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ ├── page.tsx
│ │ │ │ └── register-form.tsx
│ │ │ ├── nextjs05
│ │ │ │ ├── locale-switcher.tsx
│ │ │ │ ├── metadata.ts
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── nextjs06
│ │ │ │ ├── error.tsx
│ │ │ │ ├── fetch-check.tsx
│ │ │ │ ├── page.module.scss
│ │ │ │ └── page.tsx
│ │ │ ├── page.module.scss
│ │ │ └── page.tsx
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ └── redux
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ ├── redux01
│ │ │ ├── child.tsx
│ │ │ ├── counter-slice.ts
│ │ │ ├── grandchild.tsx
│ │ │ ├── hooks.ts
│ │ │ ├── myself.tsx
│ │ │ ├── page.module.scss
│ │ │ ├── page.tsx
│ │ │ └── store.ts
│ │ └── redux02
│ │ ├── child.tsx
│ │ ├── hooks.ts
│ │ ├── image-box.tsx
│ │ ├── image-slice.ts
│ │ ├── page.module.scss
│ │ ├── page.tsx
│ │ ├── store.ts
│ │ ├── text-box.tsx
│ │ └── text-slice.ts
│ ├── lib
│ │ ├── common
│ │ │ ├── db
│ │ │ │ ├── pool-config.ts
│ │ │ │ └── pool.ts
│ │ │ ├── definitions.ts
│ │ │ ├── i18n
│ │ │ │ ├── dictionaries
│ │ │ │ │ ├── cn.json
│ │ │ │ │ ├── en.json
│ │ │ │ │ ├── ja.json
│ │ │ │ │ └── ko.json
│ │ │ │ ├── dictionaries.ts
│ │ │ │ └── i18n-config.ts
│ │ │ └── sidebar-links.tsx
│ │ ├── components
│ │ │ ├── alert-snackbar.tsx
│ │ │ ├── go-back.tsx
│ │ │ ├── msg-snackbar.tsx
│ │ │ └── spacer.tsx
│ │ ├── firebase
│ │ │ ├── firebase.ts
│ │ │ ├── firestore-data-converter.ts
│ │ │ └── firestore-helper.ts
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ ├── sidebar.tsx
│ │ └── utils
│ │ └── util.ts
│ └── scss
│ └── common
│ ├── _index.scss
│ ├── _mixin.scss
│ ├── _mq.scss
│ └── _variables.scss
├── tailwind.config.ts
└── tsconfig.json
45 directories, 145 files
9. 備考
今回はFirestoreの使い方についてでした。
10. 参考
- Docs | Next.js (nextjs.org)
- Quick Start – React
- Getting Started with Redux | Redux
- Getting Started with React Redux | React Redux (react-redux.js.org)
- Material UI: React components based on Material Design (mui.com)
- Firestore | Firebase (google.com)
投稿者プロフィール
-
開発好きなシステムエンジニアです。
卓球にハマってます。