前回は、Firestore Database を使い、React でメッセージ送信機能を実装しました。

【Firebase】Firestore Databaseを使い、Reactでメッセージ送信機能を実装する
今回は、プロフィール編集画面を作成し、Firestore Storage にアバター画像を保存します。
まずは、ヘッダーのプロフィールをクリックすると、プロフィール画面へ遷移するようにします。
MUI でプロフィール画面を作りましょう。
tsx
import React, { useState } from "react";
import {
Paper,
Typography,
Box,
TextField,
Button,
Container,
} from "@mui/material";
const Profile = () => {
const [name, setName] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.files);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
};
return (
<Container maxWidth="sm">
<Paper sx={{ m: 4, p: 4 }}>
<Typography align="center">プロフィール編集</Typography>
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 4 }}>
<input type="file" accept="image/*" onChange={handleChange} />
<TextField
margin="normal"
required
fullWidth
id="name"
label="ユーザー名"
name="name"
autoComplete="name"
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
保存
</Button>
</Box>
</Paper>
</Container>
);
};
export default Profile;

次に、react-router-domでプロフィール画面のパスを作成します。
App.tsx へ移動し、Routeを追加します。
tsx
<Route path="profile" element={<Profile />} />
Header.tsx へ移動し、リンクを設定しましょう。
tsx
<MenuItem onClick={handleClose}>
<Link href="profile" underline="none" color="inherit">
プロフィール
</Link>
</MenuItem>
プロフィール画面もヘッダーを表示させたいので、App.tsx でホーム画面とプロフィール画面のみヘッダーを表示するようにします。
react-router-domからOutletをインポートします。
tsx
import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";
Outletがchildrenみたいな役割を果たしてくれます。
Layout関数を作成します。
Layout関数の中にHeaderとOutletを設定します。
tsx
function Layout() {
return (
<>
<Header />
<Outlet />
</>
);
}
App関数のRoutesの中にLayoutを設定します。
tsx
function App() {
return (
<ThemeProvider theme={theme}>
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="profile" element={<Profile />} />
</Route>
<Route path="login" element={<Login />} />
<Route path="signup" element={<Signup />} />
<Route path="password-reset" element={<PasswordReset />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
プロフィール画面を確認すると、

ヘッダーが表示されました。
ちなみに、ログイン画面では、

ヘッダーが表示されていません。
このままでは、ホーム画面でヘッダーが二重で表示されるので、Home.tsx のHeaderは削除しておきましょう。
『ファイルを選択』をクリックすると、画像を選択できるようになっています。

HTML のレイアウトではなく、MUI のボタンを作成し、全体を統一します。
まずは、inputタグの下にボタンを作成します。
tsx
<Button variant="contained" color="primary" component="span">
画像を選択
</Button>
inputタグにidを追加します。
また、Buttonを label タグで囲みます。
labelタグにhtmlForを設定し、inputタグのidを指定します。
inputタグをdisplay:noneで非表示にします。
tsx
<input
id="image"
type="file"
accept="image/*"
onChange={handleChange}
style={{ display: "none" }}
/>
<label htmlFor="image">
<Button variant="contained" color="primary" component="span">
画像を選択
</Button>
</label>
では、動作確認してみます。

画像を追加し、Console を確認すると、

画像のデータが表示されました。
このままでは、画像が選択されているか、画面では分からないので、画面に画像を表示させます。
useState で画像データの状態を管理しましょう。
tsx
const [image, setImage] = useState<File | null>();
tsx
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files !== null) {
setImage(e.target.files[0]);
}
};
次に、MUI のAvatarを使い、画像を表示する場所を作成します。
Avatarの src には、imageがある場合、URL.createObjectURLでimageを指定します。
imageがない場合は、””としておきましょう。
tsx
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Avatar src={image ? URL.createObjectURL(image) : ""} alt="" />
<div>
<input
id="image"
type="file"
accept="image/*"
onChange={handleChange}
style={{ display: "none" }}
/>
<label htmlFor="image">
<Button variant="contained" color="primary" component="span">
画像を選択
</Button>
</label>
</div>
</Box>

では、画像を選択してみます。

Avatar が設定されました。
画像が選択できたので、次は、Firebase の Storage に画像が保存できるようにします。
Firebase の Storage にアクセスします。
Rules タグをクリックします。
今のところ、ファイルの read や write が禁止されています。

こちらを、認証されている場合は許可するようにします。
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
公開をクリックします。

Profile.tsx へ戻ります。
以前 Firebase の初期設定した、Firebase フォルダの firebaseConfig からfirebaseAppをインポートします。
Firebase の設定は、こちらをご覧ください。

【Firebase】Firebase Project Configを設定する

【Firebase】Firestore Databeseのデータを、フロントエンドに表示する

【Firebase】Storageで保存した画像をブラウザに表示する
firebaseAppのfirestorageを使用します。
tsx
const firestorage = firebaseApp.firestorage;
handleSubmit 関数内で、try/catch を使います。
catch の場合は、エラーメッセージを表示するようにします。
tsx
const [error, setError] = useState(false);
tsx
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
try {
} catch (err) {
console.log(err);
setError(true);
}
};
tsx
{
error && <Alert severity="error">送信できませんでした</Alert>;
}
firebase/storageからrefをインポートします。
tsx
import { ref } from "firebase/storage";
ref の第一引数に、先程設定したfirestorage、第二引数にファイル名としてimageオブジェクトのnameを指定します。
tsx
try {
if (image) {
const imageRef = ref(firestorage, image.name);
}
} catch (err) {
console.log(err);
setError(true);
}
firebase/storageからuploadBytesをインポートします。
tsx
import { ref, uploadBytes } from "firebase/storage";
uploadBytesの第一引数に imageRef、第二引数に image を指定します。
thenでConsoleに送信内容を表示するようにします。
tsx
try {
if (image) {
const imageRef = ref(firestorage, image.name);
uploadBytes(imageRef, image).then((snapshot) => {
console.log("Uploaded a file!", snapshot);
});
}
} catch (err) {
console.log(err);
setError(true);
}
では、動作確認してみます。

『保存』をクリックすると、

画像が送信されたようです、
Firebase の Storage を確認してみましょう。

画像が保存されていました。
次回は、

【Firebase】プロフィール画面で作成したユーザー情報を、Firestore Databaseに保存する