♨️

【React】firebase storageにリサイズした画像をアップロード

2021/07/25に公開

はじめに

https://note.com/jep_subject/n/n352511283401
React+firebaseでマッチングアプリを作成している者です。
アイコンを保存→リサイズする際使用した処理を書き残しておきます。


環境

"react": "^17.0.2"
"typescript": "^4.3.5"
"firebase": "^8.7.0"
"react-image-file-resizer": "^0.4.7"
"@chakra-ui/react": "^1.6.4"

EditAvatar.tsx

import { memo, useState, VFC } from 'react';
import {
  FormControl,
  FormLabel,
  useToast,
  Input,
  Text,
  Box,
  Flex,
  Spacer,
  Avatar,
  AlertStatus,
} from '@chakra-ui/react';
import Resizer from 'react-image-file-resizer';
import { db, storage } from '../../firebase';
import FormButton from '../atom/FormButton';

type Props = {
  valueAvatar: string | undefined;
  userId: string;
};

const EditAvatar: VFC<Props> = memo(({ valueAvatar, userId }) => {
  const [avatar, setAvatar] = useState<string>('');
  const [filename, setFilename] = useState<string>();
  const toast = useToast();

  const resizeFile = (file: Blob) =>
    new Promise((resolve) => {
      Resizer.imageFileResizer(
        file,
        200,
        200,
        'JPEG',
        80,
        0,
        (uri) => {
          resolve(uri);
        },
        'base64',
      );
    });

  const onSubmitAvatar = async () => {
    let toastTitle = 'アイコンを更新しました';
    let toastStatus: AlertStatus = 'success';
    try {
      // deta_url形式でアップロード
      await storage.ref(`avatars/${userId}`).putString(avatar, 'data_url');
      // アップしたurlを受け取ってfirestoreに保存
      const url = (await storage
        .ref('avatars')
        .child(userId)
        .getDownloadURL()) as string;
      await db.collection('users').doc(userId).update({ avatar: url });
    } catch (error) {
      toastTitle = 'アップロードに失敗しました';
      toastStatus = 'error';
    } finally {
      // アップロードの結果をトーストで表示
      setAvatar('');
      setFilename('');
      toast({
        title: toastTitle,
        status: toastStatus,
        position: 'top',
        duration: 9000,
        isClosable: true,
      });
    }
  };

  const onChangeAvatar = async (e: React.ChangeEvent<HTMLInputElement>) => {
    // ファイル名表示用
    const fileImage = e.target.files?.[0].name;
    setFilename(fileImage);
    const blobImage = e.target.files?.[0] as Blob;
    // 空ファイルを変換しないように
    if (blobImage !== undefined) {
      // 画像のみを受け付ける
      if (/image.*/.exec(blobImage.type)) {
        const resizeImage = (await resizeFile(blobImage)) as string;
        setAvatar(resizeImage);
      } else {
        toast({
          title: '画像のみアップロードできます',
          status: 'warning',
          position: 'top',
          duration: 9000,
          isClosable: true,
        });
      }
    }
  };

  return (
    <>
      <Avatar src={avatar || valueAvatar} marginY="10px" marginX="5px" />

      <Text color="header" marginBottom="5px" marginX="5px">
        アイコン
      </Text>

      <form onSubmit={onSubmitAvatar}>
        <FormControl>
          <Flex marginX="5px" borderBottom="1px" borderColor="secondary">
            <FormLabel
              htmlFor="avatar"
              color="link"
              whiteSpace="nowrap"
              border="1px"
              borderColor="link"
              borderRadius="md"
              display="inline-block"
              padding="7px"
              height="40px"
              cursor="pointer"
            >
              ファイルを選択
              <Input
                display="none"
                type="file"
                id="avatar"
                name="avatar"
                placeholder="ヘッダー"
                onChange={onChangeAvatar}
                accept="image/*"
              />
            </FormLabel>
            <Box
              paddingTop="8px"
              style={{
                textOverflow: 'ellipsis',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                width: '200px',
              }}
            >
              {filename || '選択されていません'}
            </Box>

            <Spacer />
            <Box display="inline-block">
              <FormButton
                onClick={onSubmitAvatar}
                display="inline-block"
                isDisabled={!filename}
              >
                更新する
              </FormButton>
            </Box>
          </Flex>
        </FormControl>
      </form>
    </>
  );
});

export default EditAvatar;

説明

説明したいことはだいたいコメントでしてますが少し補足しておきます。

フォームのonChange処理

const onChangeAvatar = async (e: React.ChangeEvent<HTMLInputElement>) => {
    // ファイル名表示用
    const fileImage = e.target.files?.[0].name;
    setFilename(fileImage);
    const blobImage = e.target.files?.[0] as Blob;
    // 空ファイルを変換しないように
    if (blobImage !== undefined) {
      // 画像のみを受け付ける
      if (/image.*/.exec(blobImage.type)) {
        const resizeImage = (await resizeFile(blobImage)) as string;
        setAvatar(resizeImage);
      } else {
        toast({
          title: '画像のみアップロードできます',
          status: 'warning',
          position: 'top',
          duration: 9000,
          isClosable: true,
        });
      }
    }
  };

画像アップロードボタンを押してからキャンセルすると無を変換してしまうのでifで囲っています。
画像が入った場合は下記のresizeImageで画像を変換しuseStateに値を保存します。
引数は型推論でBlobを受け取るようなので事前にasで定義しました。

risizeFile設定

const resizeFile = (file: Blob) =>
    new Promise((resolve) => {
      Resizer.imageFileResizer(
        file,
        200,
        200,
        'JPEG',
        80,
        0,
        (uri) => {
          resolve(uri);
        },
        'base64',
      );
    });

この設定だと縦200,横200のjpeg,画質80(100まで)になります。

onSubmit処理

const onSubmitAvatar = async () => {
    let toastTitle = 'アイコンを更新しました';
    let toastStatus: AlertStatus = 'success';
    try {
      // deta_url形式でアップロード
      await storage.ref(`avatars/${userId}`).putString(avatar, 'data_url');
      // アップしたurlを受け取ってfirestoreに保存
      const url = (await storage
        .ref('avatars')
        .child(userId)
        .getDownloadURL()) as string;
      await db.collection('users').doc(userId).update({ avatar: url });
    } catch (error) {
      toastTitle = 'アップロードに失敗しました';
      toastStatus = 'error';
    } finally {
      // アップロードの結果をトーストで表示
      setAvatar('');
      setFilename('');
      toast({
        title: toastTitle,
        status: toastStatus,
        position: 'top',
        duration: 9000,
        isClosable: true,
      });
    }
  };

Resizerはdata_url形式を値を返すのでputString(avatar, 'data_url')でstorageにアップロードしています。
ファイル名はavatar/userIdになっているので新しいものは上書きされる仕様になってますが新規保存したい場合は適宜ランダムな文字列を返す関数などで名前を変えて下さい。

参考にした資料

https://firebase.google.com/docs/storage/web/upload-files?hl=ja
https://zenn.dev/masalib/books/2d6e8470732c8b
https://ichi.pro/react-image-fileresizer-o-shiyoshite-react-de-appu-ro-do-suru-mae-ni-gazo-o-asshukusuru-212785776283463

Discussion