React FCで画像ファイルを『Content-type: multipart/form-data』でPOSTする方法(axios)


案件でPOSTする際、
『Content-type: form-data』で送信する機会があったので、まとめます。
ボタン部分はマテリアルUIを使っています、初見の方は細かく気にしなくても大丈夫です

## 環境
react　16.12.0
typescript　3.7.3
material-ui/core　4.8.0(Buttonに使用)

##したいこと
inputで選択したファイルをstateにセット、セットしたファイルを POSTする

## 全体

```.tsx

const IconUpload: FC = () => {

  const [userIconFormData, setUserIconFormData] = useState<File>()

  const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files) return
    const iconFile:File = e.target.files[0]
    setUserIconFormData(iconFile)
  }

  const handleSubmitProfileIcon = () => {
    const iconPram = new FormData()
    if (!userIconFormData) return

    iconPram.append('user[icon]', userIconFormData)
    axios
      .post(
        'https://api/update',
        iconPram,
        {
          headers: {
            'content-type': 'multipart/form-data',
          },
        }
      )
  }

  return (          
     <form>
       <p>アイコンアップロード</p>
       <input
         type="file"
         accept="image/*,.png,.jpg,.jpeg,.gif"
         onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)}
       />
       <Button
         text="変更する"
         variant="contained"
         color="primary"
         type="button"
         onClick={handleSubmitProfileIcon}
         disabled={userIconPreview === undefined}
       />
    </form>
  )
}

export default IconUpload
```

##切り分けて解説
###form周り、ボタン部分
```tsx
　　<form>
       <p>アイコンアップロード</p>
       <input
         type="file"
         accept="image/*,.png,.jpg,.jpeg,.gif"
         onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)}
       />
       <Button
         text="変更する"
         variant="contained"
         color="primary"
         type="button"
         onClick={handleSubmitProfileIcon}
         disabled={userIconPreview === undefined}
       />
    </form>
```
####input
- type：fileにすることによってアップロードが可能になる
- accept：アップロード画面で、ここに指定された拡張子のファイルのみ選択可能になる
- onChange：通常のinputと異なり、valueにはFileを入れることはできない、なので別の場所に取得したFileを保持するfunctionを呼び出す

####Button
- onClick：onChangeでセットしたstateをaxiosでPOSTするfunctionを呼び出す

###ファイルを取得するfunction
```.tsx
const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => {
  if (!e.target.files) return
  const iconFile:File = e.target.files[0]
  setUserIconFormData(iconFile)
}
```
``if (!e.target.files) return``
TypeScriptでは、undefindになる可能性のある値に関してはエラーがでるので先に無い場合はreturnすることを明示的にしている

``const iconFile:File = e.target.files[0]``
これがファイル本体。
filesは複数選択可能の場合を備え、配列になっていて、[0]としてあげないと取得できない。
型はFile。

``setUserIconFormData(iconFile)``
stateにセット

####ちなみに、`e.target.files[0]`をURLにしてimgに入れたいとなると
```.jsx
const blobUrl = URL.createObjectURL(iconFile)
```
`blob:http://パス`に変換され、URLをしてとして扱うことができます。


###ファイルをaxiosでPOSTするfunction

```.tsx
const createProfileIcon = () => {
    const iconPram = new FormData()
    if (!userIconFormData) return

    iconPram.append('user[icon]', userIconFormData)
    axios
      .post(
        'https://api/update',
        iconPram,
        {
          headers: {
            'content-type': 'multipart/form-data',
          },
        }
      )
  }
```
ようやくAPI送信部分
`content-type: multipart/form-data`で送信する際、気をつけること２つ

####1. `FormData`という形式で送ってあげなければいけない。

`const iconPram = new FormData()`
FormDataオブジェクトを作成
型はそのまんまで、`FormData`

`iconPram.append('user[icon]', userIconFormData)`
作成したFormDataオブジェクトにパラメーターを追加
**append(キー, 送信したいファイル)**


####2. headersに`'content-type': 'multipart/form-data'`を指定
基本API通信する時はjsonが多いかと思いますが、jsonでは正しく送れず、空になるので注意

`_axios.create`を使って、Classでまとめている場合にも、

```.tsx
className.defaults.headers = { 'Content-Type': 'multipart/form-data' }
```
で、上書きしてあげましょう

##送信内容

<img width="573" alt="スクリーンショット 2020-03-22 16.48.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155363/49f9716e-ff69-65f1-3029-cc508144eaeb.png">

こんな感じです。
###確認ポイント
1. **RequestHeaders**の`content-type`が**`multipart/form-data`**になっている
2. FormDataのvalueが**`(binary)`**になっている

##まとめ
これで正常にmultipart/form-dataでファイルをPOSTできるようになりました！
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです

初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、２種類のみのようです。

- Fileのまま送信（multipart/form-data）
- Base64にエンコードして送信（application/json）


エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね