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);
これで描画の速度面は問題なくなったしこれでなんとかって感じ。ファイル周りはどうにかしたい。
ReactとRedux触ってみた
TL;DR
- ReactとRedux触りながら勉強してますよー
- Reactのライフサイクル周りよくわかってない
- redux-form触ってて一見便利そうだったけどクソ重事案に引っかかって泣きそう
- そんな感じの日記(別に知見の共有とかはない)
最近雑用みたいな仕事が増えてきててそろそろフロントエンドも出来るようになっとかないとなーと思ってたところ、そういうフロントのツールを作って欲しいって頼まれたので前に保守頼まれてたツールを派生させる形でReactのwebを1から作ることにした話。
まずReactとReduxについて(納期も短いので)簡単に学ぶ。体系的には勉強してないのでコード書きながらこんな感じに理解した。多分色々違う。
React
- SPA/PWAに対応したjavascriptのフロントのフレームワーク
- 名前の通り特定の変数をvalueとして詰めることでリアクティブに値のDOMへの反映がまとめて出来る
- それぞれのコンポーネントのライフサイクルの処理をハンドリングして適宜処理を埋め込んでいく
- propsとしてデータを子に渡せるほかコンポーネントごとにstateとして変数を管理出来る(これをもとにリアクティブに値を反映する)
- よくinputのonChangeとvalueのところに対応するstateの値とそれを変更する関数を埋め込み忘れて入力が効かねえ!ってことになったりする
Redux
- Reactとよく組み合わせて使われるツールで上記のstateを管理する
- state(やりとりされるデータ)、store(state置き場)、action(stateを変更する処理)、dispatch(actionをstoreに保存するための手続き)、reducer(actionに対してstoreにこういう処理を行うってのをまとめたもの)ってのが用いられる
- stateは原則1つのみでjsonみたいな形で保持し、複数のreducerはユニークな名前空間を指定して利用するような形
- Reactとつなぎこむことでそれ単体だと分かりづらいstateの管理を一元管理してコンポーネントをまたいで値の反映をしやすくしている
今回使ったreact関係のツールは主に以下のもの。あと管理webだったのでadminlteをガワに採用した。
- react-router-dom
- immutable
- react-intl
- redux-thunk
- react-form
immutableはstateとしてrecordをextendsしたmodel的なクラスを登録しており、set(hoge).action()とした結果をdispatchするのに使っている(action関数はclass内定義したactionを返す関数)。
使ってみてハマったこと
redux-thunk
dispatchするときはaction渡さないといけないんだけど関数渡してもそれ実行するようにさせるよってmiddleware。違うかも。
最初は使わなくてもいけるんじゃねーの?と思ったが最初はモックで適当なもの同期で返してたものをAPI呼び出しに変えたところ"Actions must be plain objects"などと言われるようになってしまい採用することに。
modelのget系関数が返す結果はthisでconnect内のdispatchするタイミングでaction呼び出す(例:hoge: dispatch(state.model.a.getHoge().action()))つもりだったのだが、APIを介した非同期だとこのgetHogeはPromiseを返すことになってしまって色々難儀した。
redux-form
form管理するためのツール。storeの一部を借りてそこにformのデータを保持してsubmitが走った時に実際にstateへの適用およびそれに伴う処理を走らせる。
これに関しては面倒な部分が複数あった。
フィールドと値の関連系の処理がめんどい
初期値を渡す部分は別に普通にreduxから渡してる値を参照させることが出来るのだが、テキストでアドレスを渡す必要がある。例えばpropsでid,nameをフィールドに持つtableという値を渡していた場合、"table[1].name"とかを名前としてフィールドに渡すことで初期値の適用やフォームの構造が定義される。
いちいちこういう文字列を生成しないといけないが、ルールとして制約できるのは個人的には悪くないかなって気がした。どうせ自前でやっても似たようなことやることになるし。
あと非同期で取ったデータをdispatchしてそれをconnectした後initialValuesに埋め込む実装を行ったが、ライフサイクルの都合か取得後もう一度値の初期化を走らせないといけなかった。reduxFormでつなぎこむ際にenableReinitialize:trueの要素を入れておけば勝手にやってくれる。が、遅くはなる。
なんか重い
これが絶賛悩んでるところで、上記のような理由で値の初期化の回数が増えるのも影響としてあるのだが、フィールドが生成されるごとに"REGISTER_FIELD"のイベントが走ってDOMの更新が行われるだけではなく、親コンポーネントのcomponentDidMountも実行されてえらいことになってる。
具体的には総フィールド数が20で上のコンポーネントのcomponentDidMountでログイン確認のためのAPIを呼んでいる場合、20(フィールド数)x2(再初期化の分)=40回上のcomponentDidMountが走ってAPI呼び出しも行われるという始末になってる。
最後に書いたredux-formの遅さがこのアプリ開発の上での最後の問題で、これにぶち当たったのがGW入る1時間前とかだった。formikとかいうのがredux-formの代替として挙げられてるのを見たので触ってみようかと思う。
なぜ他の人たちがこのそうすることを勧めているのか、についてはその後ゆっくり腰を据えて考えてみようと思う。目下は場当たり的に物事に当たるスタンスでとりあえず乗り切りたい。