redux-formからformikに置き換える

spamoc.hatenablog.com  最初のエントリーで書いたが、formik使ってみたかったので試したら案外簡単に移行できたしやりたいことも達成できたのでメモしとく。

redux-formで書いたソース(簡略版)

const MyForm = ({handleSubmit}) => (
  <form onSubmit={handleSubmit}>
    <Field
      name={"records[0].a"}
      component={MyField}
    />
  </form>
);
const Hoge = connect(state => ({
  onSubmit: (value) => console.log(value),
  initialValues: {
    records: state.model.hoge.list
  }}
))(MyForm);
export const HogeForm = reduxForm({
  form: "HogeForm",
  enableReinitialize: true
})(MyForm);

(動作の保証はないが)こんな感じのコードを書いていた。

FieldのnameにinitialValuesのオブジェクトの参照パスを文字列で渡すのが若干気持ち悪いが、そうするとvalueに当ててくれるしsubmitしたときの値と対応付けてくれるしとてもありがたい。 あといちいちonChangeで取った値をsetStateとかで対応付けたりする必要もない。

が、フィールド数が多くなるとやたら遅くなる問題にぶち当たって断念した。

断念したときのフィールド数は200を超えていた。多分アプリケーションとしての設計が間違ってると思う。

Formikへの移行

Formikはredux-formのカウンター的に作られてるんだろうなあってくらいに似た構造で出来ていた。強いて言うとreduxによる結合がないという部分が大きな違いで、どうやらreduxではなく親タグでフォームの値を管理することでその辺カバーしている様子。

なのでやることは単純で以下の変更を加えるだけで動いた。

  • reduxFormの記述の除去
  • formの上にFormikタグの設置
  • import文の整理

Formikで書いたソース(簡略版)

class MyForm extends React.Component {
  render() {
    const { initialValues, onSubmit } = this.props;
    return (
      <Formik
        initialValues={initialValues}
        enableReinitialize={true}
        onSubmit={onSubmit}
      >
        {props => <MyFormBody {...props} {...this.props} />}
      </Formik>
    );
  }
}
const MyFormBody = ({handleSubmit}) => (
  <form onSubmit={handleSubmit}>
    <FastField
      name={"records[0].a"}
      component={MyField}
    />
  </form>
);
export const HogeForm = connect(state => ({
  onSubmit: (value) => console.log(value),
  initialValues: {
    records: state.model.hoge.list
  }}
))(MyForm);

{props => <MyFormBody {...props} {...this.props} />}となってる部分がキモで、こうしないとFormik自体が渡すpropとこっちで渡したいpropががっちゃんこできない。他にもrender属性にjsxを書くことでも同じことはできるがなんか嫌だったので上記の書き方をしている。

あとFastFieldを使用している。多くのフィールドを表示するとき用の性能最適化版で再描画がコントロールされてる模様。

問題

これを使ったときの最大の問題がファイルのアップロードで、Formik1.5.4では<input type="file">には対応していなかった。そのためファイルを上げる必要がある部分だけは別でこしらえる必要があった。

仕方ないのでFormikもどきを作って難を逃れた。正直ファイルを上げる以外には使えないゴミなので注意。

Formikもどき:

export class FileForm extends React.Component {
  handleSubmit = e => {
    e.preventDefault();
    const { onSubmit } = this.props;
    return onSubmit(this.state.files);
  };
  handleChange = e => {
    if (e.persist) {
      e.persist();
    }
    this.setState(prevState => ({ files: e.target.files }));
  };
  getProps() {
    return {
      handleSubmit: this.handleSubmit,
      handleChange: this.handleChange
    };
  }
  render() {
    const { children } = this.props;
    const props = this.getProps();
    return children(props);
  }
}
export class Field extends React.Component {
  render() {
    const { component, children, ...props } = this.props;
    console.log(props);
    return React.createElement(component, {
      ...props,
      children
    });
  }
}

実装部(csvを上げる想定):

const CSVField = ({ name, label, onChange }) => (
  <input type="file" accept=".csv" id={name} className={name} name={name} onChange={onChange} />
);
class CSVForm extends React.Component {
  render() {
    const { onSubmit } = this.props;
    return (
      <FileForm onSubmit={onSubmit}>
        {props => <CSVFormBody {...this.props} {...props} />}
      </FileForm>
    );
  }
}
const CSVFormBody = ({ handleSubmit, handleChange, label }) => (
  <form onSubmit={handleSubmit} id="csvupload">
    <Field
      name="csvfile"
      label={label}
      onChange={handleChange}
      component={CSVField}
    />
    <button type="submit">submit</button>
  </form>
);
export const CSVUploadView = connect(
  state => ({ onSubmit: (data) => console.log(data[0])})
)(CSVForm);

これで描画の速度面は問題なくなったしこれでなんとかって感じ。ファイル周りはどうにかしたい。