react-jsonschema-formで外部バリデーションを使用する方法
どうも、くずきです。
こないだReact
でフォームを作る際にreact-jsonschema-formと呼ばれる便利なライブラリを使ったんですが、外部からのバリデーション入れるのに詰まったのでその解決法を書きたいとお見ます。
react-jsonschema-formとは
React
でフォーム作る際に、JSON
で設定ファイルを書いてしまえば自動的に作ってくれるという便利なものです。
Github
のスターが2300(2017/10月時)もあり、色々なところで使われているライブラリです。
簡単に作れるのでお試しで作りたいとか、デザイナーいないからそれぽいの作りたい時に重宝されるみたい。
自分的にはこのライブラリは独自のフォームの作成にも対応していて、インターフェースを統一しながら自分のプロダクトに合わせたものが作れるのが強みなのではないかと思う。
(近々カスタムフォーム周りの説明は記事にします)
バリデーションについて
このライブラリではバリデーションも簡単で、JSON
にバリデーションの内容を書くだけでエラー文言を出してくれます。
また、カスタムのバリデーションに関しても、
function validate(formData, errors) { if (formData.pass1 !== formData.pass2) { errors.pass2.addError("Passwords don't match"); } return errors; } const schema = { type: "object", properties: { pass1: {type: "string", minLength: 3}, pass2: {type: "string", minLength: 3}, } }; render(( <Form schema={schema} validate={validate} /> ), document.getElementById("app"));
このように、pass1
とpass2
にはJSON
で長さのバリデーションをつけ、validate
関数によってカスタムのバリデーションを追加できます。
しかしながら、弊社のプロダクトではアカウントの登録時にemail
の重複やサーバー側とフロント側でバリデーションを揃えたいという要望があり、調べた限りですと対応していないようでした。
Storeを利用した外部バリデーション
上記の問題を解決するために、すべてのForm
をカスタムフォームにし、
Form(error用のStoreに登録) -> Action(サーバーからのレスポンス) -> Store(エラー登録) -> Form(Storeからエラーを受け取ったら表示)
のような流れで実装しました。
Form
のコンポーネント
class SelectField extends Component { constructor(props) { super(props); this.state = { formData: props.formData?props.formData : null, errors: null }; this.handleError = this.handleError.bind(this); } componentWillReceiveProps(props) { this.setState({ formData: props.formData }); } componentWillMount() { errorStore.addValidListener(this.handleError); } componentWillUnmount() { errorStore.removeValidListener(this.handleError); } handleError(_errors) { let errors = Validation.getValidationErrors(_errors, this.props.name); this.setState({ errors: errors }); } render() { return ( ... ); } }
特段珍しいことはせず、フォームごとにerrorStore
に登録。
バリデーションの中身はすべてきてしまうため、そのフォームのエラーがあるかgetValidationErrors
で検索。
一応getValidatonErrors
export function getValidationErrors(errors, name) { let res = []; _.map(errors, (v,k) => { if (k === name) { _.map(v, (v2, k2) => { res.push(v2); }); } }); return res; }
Action
export function itemEdit(data, id) { let url = API.Item.Edit.replace(":id", id); request .post(url) .send(data) .use(AuthUtil.bearer) .end(function(err, res){ if (err === null) { let body = res.body.success; dispatcher.dispatch({type:"ITEM_EDIT", body}); } else { dispatcher.dispatch({type:"ERROR", res}); // レスポンスがエラー時はErrorStoreに投げる } }); }
エラーだった際にErrorStore
に投げる。
ErrorStore
class ErrorStore extends EventEmitter { handleActions(action) { if(action.type === "ERROR"){ // エラー通知 if(action.res.hasOwnProperty("statusCode")) { switch (action.res.statusCode) { case 500: break; case 400: this.emit(EVENT.ERROR.VALIDATION, action.res.body.errors); break; case 401: this.emit(EVENT.ERROR.AUTHENTICATION, action.res); break; default: break; } } } } addValidListener(listener) { this.on(EVENT.ERROR.VALIDATION, listener); } removeValidListener(listener) { this.removeListener(EVENT.ERROR.VALIDATION, listener); } } const errorStore = new ErrorStore(); dispatcher.register(errorStore.handleActions.bind(errorStore)); export default errorStore;
ErrorStore
ではステータスコードによって通知先(emit)を変えてるので、いろいろなエラーに対応できる。(今の所)
問題点
このやり方だと問題があり、フォームごとにStore
に登録するためEventEmitter.defaultMaxListeners
の上限を変えないと怒られてしまいます。
そもそも登録しすぎるとメモリ食うのでフォームを登録している親コンポーネントがErrorStore
に登録し、子のフォームに伝えるやり方がパフォーマンス的には良いのかなと思ってますが・・・なるべく独立して使いやすくしたい。。
おまけ
ここで議論されてるっぽい。(Redux-formだとそれっぽいこと簡単にできそう・・・・)