react + redux-saga でファイルアップロード

react 初心者がいろいろ学んだことやハマったことの記録。
今回はredux-saga + axios を使ってファイルアップロードするお話。

## Uploaderコンポーネントを作成する

Reactの公式サイト「[非制御コンポーネント](https://ja.reactjs.org/docs/uncontrolled-components.html#the-file-input-tag)」を参照して、ファイル選択するinputを用意します。
再利用できるよう、Uploaderコンポーネントとして作成してみます。

```tsx:Uploader.tsx

import React from 'react';

const Uploader: React.FC = () => {
  const uploadFile = React.createRef<HTMLInputElement>();

  const handleClick = (event) => {
    // ここにアップロード処理を入れる予定
  };
  
  return (
    <>
      <div className="uploader-file">
        <label>アップロードファイル：
          <input type="file" name="upload" ref={uploadFile} />
        </label>
      </div>
      <input type="button" value="アップロード" onClick={handleClick}/>
    </>
  );  
};

export default Uploader;
```

Reactでは、プログラムからfileタイプのinputには直接操作ができないとのことで、Refを使って対象のDOMノードを参照するようにします。
こうすることで、選択された[Fileオブジェクト](https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications)が取得できます。

```typescript
const file = uploadFile.current.files[0];
```

## Redux-Sagaを使ってアップロードする

アップロード処理自体は、redux-sagaを使ってみます。
まず、アップロードAPIのリクエストとレスポンスのモデル（こちらは割愛）とActionTypeを定義しておき、`typesafe-actions`を使ってActionCreatorを定義します。

```ts:actions.ts
import {createAsyncAction} from 'typesafe-actions';
import * as types from './types';
import {UploadFileRequest, UploadFileResponse} from './models';

export const uploadFile = createAsyncAction(
  types.UPLOAD_FILE_REQUEST,
  types.UPLOAD_FILE_SUCCESS,
  types.UPLOAD_FILE_FAILURE,
)<UploadFileRequest, UploadFileResponse, Error>();
```

sagaは、今回の例では単一ファイルのみをアップロード対象としました。
複数ファイル選択可能とする場合は、FileListの型としてリクエストに定義しておけば、複数ファイルをアップロードすることも可能です。

```ts:sagas.tsx
import {SagaIterator} from '@redux-saga/core';
import {getType} from 'typesafe-actions';
import {call, put, takeLatest} from 'redux-saga/effects';
import axios from 'axios'

// axiosでアップロード
const uploadApi = (formData) => {
  return axios.post(
    '/some/path',
    formData, 
    {
      headers: {
        'content-type': 'multipart/form-data',
      },
    });
};

export function* uploadFile(
  action: ReturnType<typeof actions.uploadFile.request>,
): SagaIterator {
  try {
    const formData = new FormData();
    formData.append('uploadFile', action.payload.file);
    const response = yield call(uploadApi, formData);
    yield put(actions.uploadFile.success(response.data));
  } catch (error) {
    yield put(actions.uploadFile.failure(error));
  }
}

export default function*() {
  yield takeLatest(getType(actions.uploadFile.request), uploadFile);
}
```

Uploaderに戻り、アップロードボタンのクリックイベントにアップロード処理の呼び出しを行います。

```tsx:Uplodaer.tsx

import React, {useCallback} from 'react';
import {useDispatch} from 'react-redux';
import * as actions from './actions';

const Uploader: React.FC = () => {
  const dispatch = useDispatch();  // 追加
  const uploadFile = React.createRef<HTMLInputElement>();

  // APIの定義を追加
  const api = {
    // アップロードAPI
    uploadFile: useCallback(
      (file) => {
        dispatch(actions.uploadFile.request({file}));
      },
      [dispatch],
    ),
  };

  // アップロードボタンクリックイベントを実装
  const handleClick = (event) => {
    const file = uploadFile.current.files[0];
    api.uploadFile(file);
  };

  return (
    <>
      <div className="uploader-file">
        <label>アップロードファイル：
          <input type="file" name="upload" ref={uploadFile} />
        </label>
      </div>
      <input type="button" value="アップロード" onClick={handleClick}/>
    </>
  );  
};

export default Uploader;
```

アップロードAPIコール関数はuseCallbackを使ってメモ化しておきます。
uploadFileリクエストActionをdispatchすると、sagasで定義したuploadFileが実行されます。

他にファイルのドラッグ＆ドロップ操作でファイル選択する方法もあるので、そちらも別の機会にじっくり試してみたいなと思ってます。

## 参考サイト

React公式サイトの非制御コンポーネント - https://ja.reactjs.org/docs/uncontrolled-components.html
react-redux - https://github.com/redux-saga/redux-saga/blob/master/README_ja.md
MDN WEB Docs のフォームデータの送信 - https://developer.mozilla.org/ja/docs/Learn/HTML/Forms/Sending_and_retrieving_form_data