はじめに
この記事はReactを勉強しはじめて、firebaseの偉大さを知り、個人開発レベルなら無料でも十分大活躍のその機能をもっと知りたいと日々試行錯誤していたとき、firebaseのstrageには画像を保存できることはわかったけど、それだけじゃサイトに投稿した画像を表示したり、他のものと一緒に投稿できないやんと思いこの記事を書いています
なのでここから下では画像と文字列をセットで投稿し、投稿されたものをサイト内で一覧表示する方法について書いています。
やること
Reactでfirebaseのfirestoreとstorageの機能を使って画像と文字列をセットで投稿し、表示する機能を作成していきます
storageに画像をアップロードし、そのアップロードした画像と文字をセットで保存しておくためにfirestoreを使います
firebaseの設定
https://console.firebase.google.com/?hl=ja
プロジェクト作成→ウェブアプリ追加
コピーしておく↓↓↓
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
const firebaseConfig = {
apiKey: "***************",
authDomain: "***************",
projectId: "***************",
storageBucket: "***************",
messagingSenderId: "***************",
appId: "***************",
measurementId: "***************"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
Firebase DatabaseとStorageの設定をしていく
「構築」の中に2つともあります
テストモードで開始します
Reactアプリの作成
npx create-react-app my-app --template typescript
npm install firebase
npm install image-validator
image-validator
は投稿する画像を制限するときに使います
(今回はファイルサイズ3GB未満かつ画像ファイルのみ)
src/
├ components/
├ Post.tsx
└ PreviewImage.tsx
├ hooks/
└ useImages.tsx
├ lib/
└ firebase.ts
├ types/
└ image.d.ts
├ utils/
└ formatTimestamp.ts
└ App.tsx
...
小話
vscodeでtsrafce
って書いてTabをおすと雛形作ってくれます
各ファイルの中身
import React from "react";
import "./App.css"
import Post from "./components/Post";
import useImages from "./hooks/useImages";
import PreviewImage from "./components/PreviewImage";
const App: React.FC = () => {
const { allImages, imagesError } = useImages();
return (
<div className="App">
<Post />
<h3>投稿一覧</h3>
<p style={{ color: "red" }}>{imagesError && imagesError}</p>
<div className="container">
<div className="row">
{allImages.map((image, index) => (
<div className="col-4" key={index}>
<PreviewImage image={image} />
</div>
))}
</div>
</div>
</div>
);
};
export default App;
useImagesはのちに出てきますが、firestoreから投稿を全て取得する自作のhooksです
allimagesでは画像はfileName(string)として持っているだけで、storageから取って来ているわけではありません
実際に画像をstorageから取って来ているのは、PreviewImageコンポーネントで処理しています
import React, { useState, ChangeEvent } from "react";
import { ref, uploadBytes } from "firebase/storage";
import { validateImage } from "image-validator";
import { db, storage } from "../lib/firebase";
import { addDoc, collection } from "firebase/firestore";
const Post: React.FC = () => {
const [file, setFile] = useState<File | null>(null);
const [text, setText] = useState<string>("");
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// ファイルのバリデーション。3GB未満でかつ画像ファイルのみに制限している
const validateFile = async (selectedFile: File): Promise<boolean> => {
const limitFileSize = 3 * 1024 * 1024;
if (selectedFile.size > limitFileSize) {
setErrorMsg("File size is too large, please keep it under 3 GB.");
return false;
}
const isValidImage = await validateImage(selectedFile);
if (!isValidImage) {
setErrorMsg("You cannot upload anything other than image files.");
return false;
}
return true;
};
// 画像を選択する
const handleImageSelect = async (e: ChangeEvent<HTMLInputElement>) => {
setErrorMsg(null);
e.preventDefault();
const selectedFile = e.target.files?.[0];
if (selectedFile && (await validateFile(selectedFile))) {
const reader = new FileReader();
reader.onloadend = () => {
setFile(selectedFile);
setImagePreview(reader.result as string);
setErrorMsg(null);
};
reader.readAsDataURL(selectedFile);
}
};
// 画像をstorageにアップロードし、firestoreに保存する
const uploadImage = async () => {
try {
if (!file) {
setErrorMsg("File not selected.");
return;
}
const timestamp = new Date().getTime();
const uniqueFilename = `${timestamp}_${file.name}`;
const storageRef = ref(storage, `images/${uniqueFilename}`);
// storageにアップロード
await uploadBytes(storageRef, file);
// firestoreに保存
await addDoc(collection(db, "Images"), {
text,
fileName: uniqueFilename,
timestamp: new Date(),
});
setErrorMsg("Submission completed!");
} catch (e) {
setErrorMsg(`Error: ${e}`);
}
};
return (
<div>
<form>
<input type="file" onChange={handleImageSelect} />
<br />
<input
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
setErrorMsg(null);
}}
/>
<br />
<a
style={{ cursor: "pointer", border: "1px solid gray" }}
onClick={uploadImage}
>
upload
</a>
</form>
<p style={{ color: "red" }}>{errorMsg && errorMsg}</p>
{imagePreview && (
<img
src={imagePreview}
style={{
width: "auto",
height: 200,
objectFit: "cover",
}}
alt="preview"
/>
)}
</div>
);
};
export default Post;
画像ファイルがユーザーから選択されたら、テキストとともにfirebaseに送る処理をしている
firestoreには、現在時刻_選択されたファイル名
という形でfileNameに保存してある(string)
import React, { useState, useEffect } from "react";
import Image from "../types/image";
import { formatTimestamp } from "../utils/formatTimestamp";
import { ref, getDownloadURL, StorageReference } from "firebase/storage";
import { storage } from "../lib/firebase";
const PreviewImage: React.FC<{ image: Image }> = ({ image }) => {
const [prevUrl, setPrevUrl] = useState<string>("");
const [error, setError] = useState<string>("");
useEffect(() => {
const getImageUrl = async (imageRef: StorageReference) => {
try {
const url = await getDownloadURL(imageRef);
setPrevUrl(url);
} catch (e) {
setError(`${e}`);
}
};
const imageRef = ref(storage, `images/${image.fileName}`);
getImageUrl(imageRef);
}, [image.fileName]);
return (
<div
style={{
boxShadow: "0px 4px 8px gray",
padding: 10,
margin: 10,
width: 350,
height: 400,
}}
>
<p>
<img
src={prevUrl}
alt={error}
style={{ height: 200, width: 300, objectFit: "cover" }}
/>
</p>
<p>{image.text}</p>
<p>{formatTimestamp(image.timestamp.toDate())}</p>
</div>
);
};
export default PreviewImage;
画像のfileNameや一緒に投稿されたテキスト等を受け取り表示するためのコンポーネントです
getDownloadURL
でstorageから画像を取得して表示している
import { collection, onSnapshot } from "firebase/firestore";
import { useEffect, useState } from "react";
import { db } from "../lib/firebase";
import Image from "../types/image";
const useImages = () => {
const [allImages, setAllImages] = useState<Image[]>([]);
const [imagesError, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
const unsubscribe = onSnapshot(collection(db, "Images"), (snapshot) => {
try {
const imagesData: Image[] = snapshot.docs
.map((doc) => ({ ...doc.data(), id: doc.id } as Image))
.sort((a, b) => b.timestamp.toMillis() - a.timestamp.toMillis());
setAllImages(imagesData);
} catch (error) {
setErrorMsg(`Error: ${error}`);
}
});
return () => {
unsubscribe();
};
}, []);
return { allImages, imagesError };
};
export default useImages;
firestoreからImagesというコレクションの中身を全て取得しています
のちに削除機能等で使う可能性があるので一応id
も取得しています
取得したデータをtimestamp
を基準に並べ替えて返します
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getStorage } from "firebase/storage";
const firebaseConfig = {
apiKey: "**********",
authDomain: "**********",
projectId: "**********",
storageBucket: "**********",
messagingSenderId: "**********",
appId: "**********",
measurementId: "**********",
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const storage = getStorage(app);
firebaseプロジェクトを作成したときに表示されたコードを少し変えているだけです
storageとfirestoreへのアクセスに必要です
import { Timestamp } from "firebase/firestore";
interface Image {
id: string;
fileName: string;
text: number;
timestamp: Timestamp;
}
export default Image;
export const formatTimestamp = (t: Date) => {
const date = new Date(t);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}/${month}/${day} ${hours}:${minutes}`;
};
終わり〜〜
最後にindex.html
に少し追加
Bootstrap用
Gridシステム使うだけです
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.min.js" integrity="sha384-fbbOQedDUMZZ5KreZpsbe1LCZPVmfTnH7ois6mU1QK+m14rQ1l2bGBq41eYeM/fS" crossorigin="anonymous"></script>
</body>
</html>
実行
ここまで全てのコードがいけてたら、次の画像のようになっているはずです
npm start
また、投稿した画像や文字たちは、firebaseのコンソール画面で確認できます
firestore
strage
終わり
これで、画像投稿の管理ができるようになりました
ここまできたらユーザーが投稿した画像ごとに表示を切り替えたり、複数画像を投稿できるようになりますね〜〜
解説クソテキトーでしたが読んでいただきありがとうございました
何か間違っている点等ありましたらコメントください!
ばいばいき〜ん