Validating FieldArrays in Redux Form

2019-01-08

I have a Redux Form project where I’m using redux-form-validators to get easy Rails-style validations. Its docs explain how to define validators for normal scalar fields, but I thought I’d add how to do the same for a FieldArray, which is a list of zero of more sub-fields.

In my case I’m working on a time-tracking and invoicing application, where admins can edit a client. The form has regular fields for the client’s name, the invoicing frequency, etc., and then it also has “work categories”. Each client has one or more work categories, and a work category is just a name and an hourly rate. For instance you might charge one rate for design and another for development, or you might track retainer hours at $0/hour and extra hours at your normal rate.

Redux Form makes it really easy to include child records right inside the main form using FieldArray. Their docs give a nice example of how to validate those nested fields, but it’s pretty DIY and verbose.

With redux-form-validators on the other hand, it’s easy. First you define a validation object with the rules for each field, like so:

const validations = {
  name: [required()],
  paymentDue: [required(), numericality({int: true, '>=': 0})],
  // ...
};

Then you write a little validation function to pass to Redux Form:

const validate = function(values) => {
  const errors = {}
  for (let field in validations) {
    let value = values[field]
    errors[field] = validations[field].map(validateField => {
      return validateField(value, values)
    }).find(x => x) // Take the first non-null error message.
  }
  return errors
};

export default connect(mapStateToProps, mapDispatchToProps)(
  reduxForm({
    form: 'editClient',
    validate
  })(EditClientPage)
);

That is just straight from the docs. For each field it iterates over its validators and reports the first error it finds. It’s simple, but it doesn’t know how to handle nesting like with FieldArray.

But notice you can really do whatever you like. It’s a bit striking for someone used to Rails how exposed and customizable everything is here. So how can we rewrite validate to be smarter?

Ideally we’d like to support validations both on individual sub-fields, like the name of a single work category, and also on the workCategories as a whole, for example checking that you have at least one. This is exactly what the Redux Form example does above: it checks that your club has at least one member, and that each member has a first and last name.

Because this is a FieldArray, Redux Form expects a different structure for its entry in the errors object returned by validate. Normally you’d have a string value, like this:

{
  paymentDue: 'must be numeric'
}

But for a FieldArray you want to pass an array with one error object for each corresponding FieldArray element, like this:

{
  workCategories: [
    {},   // no errors
    {name: 'is required'},
    {name: 'is too short', rate: 'must be numeric'},
  ]
}

Those sub-errors will get passed in the meta object given to the component used to render each Field. Again, you can see that happening in the Redux Form example.

In addition, the array may have its own _error attribute for top-level errors. That gets passed as meta.error to the FieldArray component itself. So _error is not just a convention; it’s a “magic” attribute built into Redux Form. We want to set it too.

I want a way to define all these validations in one big object: top-level fields, the FieldArray itself, and the fields of individual FieldArray records. Here is the structure I set up:

const validations = {
  name:       [required()],
  paymentDue: [required(), numericality({int: true, '>=': 0})],
  ...
  workCategories: {
    _error: [
      required({msg: "Please enter at least one work category."}),
      length({min: 1, msg: "Please enter at least one work category."})
    ],
    name:   [required()],
    rate:   [required(), numericality({'>=': 0})],
  },
};

Then instead of the recommended validate function I used this:

const buildErrors = (validations) => (values) => {
  const errors = {};
  for (let field in validations) {
    if (field === '_error') continue;
    let value = values[field];
    const fieldValidations = validations[field];
    if (fieldValidations.constructor === Array) {
      errors[field] = fieldValidations
          .map(validateField => validateField(value, values))
          .find(x => x);
    } else {
      errors[field] = value ? value.map(o => buildErrors(fieldValidations)(o)) : [];
      if (fieldValidations._error) {
        errors[field]._error = fieldValidations._error
            .map(validateField => validateField(value, values))
            .find(x => x);
    }
  }
  return errors;
}

export default connect(mapStateToProps, mapDispatchToProps)(
  reduxForm({
    form: 'editClient',
    validate: buildErrors(validations),
  })(EditClientPage)
);

There are a few things to note here: I’m using a recursive function, where each call handles one “level” of the validations object. It iterates over the field names, and if the field has an array, it handles it as before. Otherwise it expects a nested object structured just like the outermost object, which each sub-field has its own array of validators. There may also be a list of validators under _errors, and those are handled specially. I’m using currying so that I can build a top-level validation function for reduxForm as well as nested functions for each FieldArray.

This function also supports reporting errors both on sub-fields and the whole FieldArray at the same time. That wouldn’t happen if the only FieldArray check was to have at least one element, like here, but you can imagine scenarios where you want to validate the FieldArray as a whole even when it isn’t empty.

I’m happy that this approach lets me combine the ease of redux-form-validators with FieldArray to get the best of both worlds. I also like that buildErrors is general enough that I can move it to a utility collection, and not write it separately for each form in my app.

Also: you might enjoy my follow-up article showing how to render the form Fields and FieldArrays with Pug and Bootstrap.

blog comments powered by Disqus Prev: Drawing Redux Form FieldArrays with Pug Next: Testing Your ActionMailer Configuration