• Using redux-form validation with redux-saga

    A laptop on a desk beside coffee and notebooks, suggesting JavaScript application work.

    If your app already uses redux-form and redux-saga, you can pass validation from the form to a saga through a Promise. Let’s build that bridge, starting with a synchronous check.

    For a rule that only checks a field value, redux-form’s synchronous validate function is enough:

    js
    reduxForm({
      form: 'example',
      validate: (values) => {
        const errors = {};
        if (values.test > 10) {
          errors.test = 'Oh no! Test is too big';
        }
        return errors;
      },
    });

    If validation needs external data already available through props, validate(values, props) can still handle it synchronously. To hand work to a saga, we can use asyncValidate, which returns a Promise: resolve it on success or reject it with field errors. See the redux-form v6.2 API reference.

    Here, asyncBlurFields selects test as a blur trigger. shouldAsyncValidate allows checks on that blur and on submit; returning true does not trigger validation on every value change or external state update. The rule stays simple so we can focus on the Promise bridge:

    js
    const isEmptyObject = (obj) => {
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) return false;
      }
      return true;
    };
    
    reduxForm({
      form: 'example',
      asyncBlurFields: ['test'],
      shouldAsyncValidate: () => true, // Allow checks on configured blur events and submit.
      asyncValidate: (values) =>
        new Promise((resolve, reject) => {
          const errors = {};
          if (values.test > 10) {
            errors.test = 'Oh no! Test is too big';
          }
          if (isEmptyObject(errors)) {
            resolve();
          } else {
            reject(errors);
          }
        }),
    });

    Now move the same check into a saga. The action carries the Promise’s resolve and reject callbacks, so the saga can return the result to redux-form. Reuse isEmptyObject from above:

    js
    reduxForm({
      form: 'example',
      asyncBlurFields: ['test'],
      shouldAsyncValidate: () => true, // Allow checks on configured blur events and submit.
      asyncValidate: (values, dispatch) =>
        new Promise((resolve, reject) =>
          dispatch({ type: 'validateExample', values, resolve, reject }),
        ),
    });
    
    function* watchExampleValidation() {
      yield takeEvery('validateExample', function* ({ values, resolve, reject }) {
        const errors = {};
        if (values.test > 10) {
          errors.test = 'Oh no! Test is too big';
        }
        if (isEmptyObject(errors)) {
          resolve();
        } else {
          reject(errors);
        }
      });
    }

    Import takeEvery from redux-saga/effects and start watchExampleValidation through your existing saga middleware before the form can validate. takeEvery passes each matching action to the worker. Without a running watcher, nothing settles the Promise.