React×TypeScriptを使ってユーザが選択したローカル画像を画面上に表示するとき、JavaScriptの場合と違い少し実装に困ったので記事にします。
実行環境
"@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の場合実装
<input type="file" id="input" multiple />
<div id="preview"></div>
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する。
修正後コード
export type Profile = {
userName: string;
birth__day: string;
tellNumber: string;
imageData: string;
};
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;
},
},
});
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}
/>
</>
);
}
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>
);
}
さいごに
input type="file"の場合、value値はuncontrolledの扱いになるみたいです。
そのため、stateで管理しようすると下記のような警告が表示されます。
a component changed from uncontrolled to controlled.
制御するのしないの?はっきりしてください。という警告です。
そのためこのエラーを解決しようとする場合は、useRefを使用して再度修正する必要があると思います。
修正完了したらまた記事を更新します。