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

React×TypeScriptを使ってユーザが選択したローカル画像を画面上に表示するとき、JavaScriptの場合と違い少し実装に困ったので記事にします。
## 実行環境
```json: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:html
<input type="file" id="input" multiple />
<div id="preview"></div>
```
```javascript: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オブジェクトで渡せば解決。
と思ったのですが、下記のようなエラーが発生しました。

```console error
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](https://redux.js.org/style-guide/#do-not-put-non-serializable-values-in-state-or-actions)

解決方法として以下の通りに修正しました。
* storeで扱うデータ型をfileからstirngに変更
* dispatchする際は、FileReaderオブジェクトを利用してfileをdate:url形式のstringに変換してからdispatchする。

### 修正後コード

```ts:type.ts
export type Profile = {
  userName: string;
  birth__day: string;
  tellNumber: string;
  imageData: string;
};
```
```ts: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;
    },
  },
});
```
```tsx: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}
      />
    </>
  );
}

```
```tsx: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のからご確認ください。
](https://github.com/kotohafurukawa/profile)

### さいごに
input type="file"の場合、value値はuncontrolledの扱いになるみたいです。
そのため、stateで管理しようすると下記のような警告が表示されます。
```console:警告
a component changed from uncontrolled to controlled.
```
制御するのしないの？はっきりしてください。という警告です。

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

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