1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ユーザが選択したローカル画像を画面上に表示したい

Posted at

React×TypeScriptを使ってユーザが選択したローカル画像を画面上に表示するとき、JavaScriptの場合と違い少し実装に困ったので記事にします。

実行環境

packge.json
    "@types/react": "^18.2.66",
    "@types/react-dom": "^18.2.22",
    "@typescript-eslint/eslint-plugin": "^7.2.0",
    "@typescript-eslint/parser": "^7.2.0",
    "@vitejs/plugin-react": "^4.2.1",
    "autoprefixer": "^10.4.19",
    "eslint": "^8.57.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.6",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.3",
    "typescript": "^5.2.2",
    "vite": "^5.2.0"

JavaScriptの場合実装

html
<input type="file" id="input" multiple />
<div id="preview"></div>
JavaScript
const inputElement = document.querySelector("#input");

inputElement.addEventListener("change", handleFiles, false);

function handleFiles() {
  // ここでFileListオブジェクトを取得する
  const fileList = this.files;
  // FileListオブジェクトはforEachが使えない
  // ForEachが使いた場合は、Arrayに変換してください。
  for(let i = 0; i < fileList.length; i++) {
    const file = fileList[i];
    if(!file.type.startsWith("image/")) {
      continue;
    }
    
    const preview = document.querySelector("#preview");
    const img = document.createElement("img");
    img.src = file
    preview.append(img);

    // FileReaderオブジェクトのインスタンス化
    const reader = new FileReader();
    reader.onload = (e) => {
      img.src = e.target.result;
    };

    reader.readAsDataURL(file);
  }
}

React×TypeScriptの場合

まずStateの管理をローカルファイルで個別管理するのは極力避けたいという意図から、React-reduxライブラリを使用してStateをグローバルコンテキストとして扱います。

そこで困ったのが、ユーザが選択した画像データをdispatchで投げるときのデータ型です。
最初は、Fileオブジェクトで渡せば解決。
と思ったのですが、下記のようなエラーが発生しました。

A non-serializable value was detected in an action, in the path: `payload`. Value

要約するとシリアライズされてないアクションが検出された。ということです。

じゃあこれどうすればいいのかということですが、

Redux storeやdispatchのactionの中には、Promise,Symbols,Map/Sets,functions, or class instancesのような値を設定するなとReduxのベストプラクティスで書かれてるので、その通り修正します。

参考記事
Do Not Put Non-Serializable Values in State or Actions

解決方法として以下の通りに修正しました。

  • storeで扱うデータ型をfileからstirngに変更
  • dispatchする際は、FileReaderオブジェクトを利用してfileをdate:url形式のstringに変換してからdispatchする。

修正後コード

type.ts
export type Profile = {
  userName: string;
  birth__day: string;
  tellNumber: string;
  imageData: string;
};
slice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { initialProfileState } from "./initializes";

const profileSlice = createSlice({
  name: "profile",
  initialState: initialProfileState,
  reducers: {
    setName: (state, action: PayloadAction<string>) => {
      state.userName = action.payload;
    },
    setBirthDay: (state, action: PayloadAction<string>) => {
      state.birth__day = action.payload;
    },
    setTellNumber: (state, action: PayloadAction<string>) => {
      state.tellNumber = action.payload;
    },
    setImageData: (state, action: PayloadAction<string>) => {
      state.imageData = action.payload;
    },
  },
});
UploadInput.tsx
import { Input } from "@nextui-org/react";
import { useAppDispatch } from "../reducks/hooks";
import { setImageData } from "../reducks/slices";

export default function UploadInput() {
  const dispatch = useAppDispatch();

  function onChangeHandler(e: React.ChangeEvent<HTMLInputElement>) {
    const files = e.target.files;

    if (files === null || files.length <= 0) {
      return;
    }

    const file = files[0];
    const reader = new FileReader();
    reader.onload = () => {
      dispatch(setImageData(reader.result as string));
    };
    reader.readAsDataURL(file);
  }

  return (
    <>
      <Input
        type="file"
        label="プロフィール写真"
        placeholder="file"
        labelPlacement="outside"
        accept=".png,.png"
        variant="underlined"
        onChange={onChangeHandler}
      />
    </>
  );
}

ProfileCard.tsx
import { Card, CardBody, Avatar } from "@nextui-org/react";
import { useAppSelector } from "../reducks/hooks";

export default function ProfileCard() {
  const state = useAppSelector((state) => state.profile);
  return (
    <Card className="md:w-3/6 mb-auto">
      <CardBody className="flex flex-col items-center gap-4">
        <Avatar src={state.imageData} size="lg" />
        <p className="flex w-full">
          <span className="min-w-20">お名前:</span>
          <span className=" text-wrap">{state.userName}</span>
        </p>
        <p className="flex w-full">
          <span className="min-w-20">お誕生日:</span>
          <span>{state.birth__day}</span>
        </p>
        <p className="flex w-full">
          <span className="min-w-20">電話番号:</span>
          <span>{state.tellNumber}</span>
        </p>
      </CardBody>
    </Card>
  );
}

全コードはこちらgithubのからご確認ください。

さいごに

input type="file"の場合、value値はuncontrolledの扱いになるみたいです。
そのため、stateで管理しようすると下記のような警告が表示されます。

警告
a component changed from uncontrolled to controlled.

制御するのしないの?はっきりしてください。という警告です。

そのためこのエラーを解決しようとする場合は、useRefを使用して再度修正する必要があると思います。

修正完了したらまた記事を更新します。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?