Reactで「CSV一括登録機能」 を開発した話

（2017/12/13）弊社アドベントカレンダーに追加したので気持ち程度修正しました。

##CSV一括登録機能？
下記のようなことをしたい！

- ドラッグアンドドロップでCSVをアップロード
- CSVをJSONにパース
- JSONを配列に変換し、Storeへ格納
- フォーム量産

今回は上2つの実装について書きます。

##D&D
ReactのD&Dライブラリはこんな感じ。
`react-dnd`が有名だけど、今回は`react-dropzone`を使った。
OS標準のFile Promptを呼び出すのが簡単だった。

- react-dnd
- React-Dropzone-Component
- **react-dropzone**( https://react-dropzone.js.org/ )

```js:FileUpload.js
import Dropzone from 'react-dropzone';
import Button from 'components/atoms/Button';

class FileUpload extends Component {

  constructor(props) {
    super(props);
    this.state = {
      isDragReject: false,
    };
    ...
  }

  onDrop() {
    this.setState({
      isDragReject: false,
    });
  }

  onDropRejected() {
    this.setState({
      isDragReject: true,
    });
  }

  handleReflect(results) {
    this.props.onComplete(results);
  }

  handleParseCsv(files) {...}

  render() {
    let dropzoneRef;
    return (
      <div className={style.fileUpload}>
        <div className={style.dropArea}>
          <Dropzone
            onDrop={this.onDrop}
            onDropAccepted={this.handleParseCsv}
            onDropRejected={this.onDropRejected}
            accept="text/csv, application/vnd.ms-excel"
            disableClick
            multiple={false}
            className={style.dropzone}
            activeClassName={style.active}
            rejectClassName={style.reject}
            ref={(node) => { dropzoneRef = node; }}
          >
            <p>Try dropping CSV file here, or click to select files to upload.</p>
            {(() => (this.state.isDragReject ?
              <p className={style.rejectMessage}>ファイルタイプが違います。</p>
              : '')
            )()}
          </Dropzone>
          <Button
            onClick={() => { dropzoneRef.open(); }}
          />
        </div>
      </div>
    );
  }
}
```

- stateの`isDragReject`でアップロードが失敗した時のメッセージの出し分けしてる
- 今回はCSVしか受け付けたくないので、Dropzoneコンポーネントの`accept="text/csv, application/vnd.ms-excel"`のところでファイルタイプを指定してる。MacとWindowsでCSVのファイルタイプの指定の仕方が違うらしいので2つ書いてる

## CSV to JSON
CSVをJSONにパースする部分は`papaparse`を使った。ドキュメントが豊富で使いやすかった。
`csvtojson`はよくわからんエラーが出て諦めた。issueたてたらPR来てたのでもうすぐ直るはず。

- csvtojson
- csv
- **papaparse**( http://papaparse.com/ )

## :imp::imp::imp:Shift-JIS:imp::imp::imp:
CSVは文字コードがShift-JISになってる可能性があるので、文字化けしないようにエンコードする必要ある！！！！
つらい！！！！Unicodeであって欲しい！！！
でも便利なライブラリある！！！！ありがとうございます:innocent::innocent:

**encoding.js**
https://github.com/polygonplanet/encoding.js/blob/master/README_ja.md

npmでは`encoding-japanese`という名前で配信されてる。

```js:FileUpload.js
import Papa from 'papaparse';
import Encoding from 'encoding-japanese';

class FileUpload extends Component {

  constructor(props) {
    ...
  }

  handleReflect(results) {
    this.props.onComplete(results);
  }

  handleParseCsv(files) {
    const file = files[0];
    const reader = new FileReader();
    reader.onload = (e) => {
      const codes = new Uint8Array(e.target.result);
      const encoding = Encoding.detect(codes);
      const unicodeString = Encoding.convert(codes, {
        to: 'unicode',
        from: encoding,
        type: 'string',
      });
      Papa.parse(unicodeString, {
        header: true,
        dynamicTyping: true,
        skipEmptyLines: true,
        complete: (results) => {
          this.handleReflect(results);
        },
      });
    };
    reader.readAsArrayBuffer(file);
  }

  render() {...}
}
```

- `header: true`にするとヘッダーの文字列をJSONのキーにしてくれる
- `reader.onload`でファイルの読み込みが完了したときの処理を定義する
- 読み込んだファイルを`Uint8Array`オブジェクトを使ってバイト列に変換
- `encoding.js`を使って現在の文字コードを取得
- Shift-JISをUnicodeに変換（Unicodeのときはそのまま）
- 文字コード変換したデータを`papa parse`に渡して、JSONにパースする
- 今回はCSVの中に数字が含まれていたので`dynamicTyping`を指定して、数字がstringに変換されないようにしている
- 完了したら、親コンポーネントで定義しているデータをフォームに反映する処理（`handleReflect `）を実行する
- さいごに、`reader.readAsArrayBuffer`でファイルの読み込みを実行する

今回は、文字コードの変換とCSVのパースを組み合わせるところではまって大変でした。
`Encoding.convert`の`to`に`utf-8`を指定すると実行できなかったのでUnicodeに変えたらうまくいきました。