いっきのblog

技術とか色々

react-jsonschema-formで外部バリデーションを使用する方法

どうも、くずきです。
こないだReactでフォームを作る際にreact-jsonschema-formと呼ばれる便利なライブラリを使ったんですが、外部からのバリデーション入れるのに詰まったのでその解決法を書きたいとお見ます。

react-jsonschema-formとは

github.com

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"));

このように、pass1pass2には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に登録し、子のフォームに伝えるやり方がパフォーマンス的には良いのかなと思ってますが・・・なるべく独立して使いやすくしたい。。

おまけ

github.com

ここで議論されてるっぽい。(Redux-formだとそれっぽいこと簡単にできそう・・・・)