Making Consistent React Forms Using a Higher-Order Component

derekbrimley

Derek Brimley

Posted on November 4, 2017

Making Consistent React Forms Using a Higher-Order Component

Forms can be a tricky part of a React app. While it would be nice to have a unified way to create a form, the forms also need to be customizable. Forms can have different styles, use different validation methods, and are submitted in different ways (i.e. to an API endpoint or a typical form submission). In our app, we have tried several ways of structuring forms, and now each form handles these issues slightly differently. We decided to come up with a solution that could be used throughout the whole app that will be flexible enough to handle the different cases, but also provide useful functionality.

The pattern we are using is known in some places as a Function as a Child Component. Some have labeled this an anti-pattern, but others have argued that it is more capable than normal, boring old higher-order components. For now, it works. Maybe one day we will realize the error of our ways and refactor it to the cool new pattern of the future. But today is not that day.

We wanted a minimalist component that does a few things for us:

  1. Sets the default values for each field, and keeps track of any changes, and if they have been touched.
  2. Returns an object with error messages.
  3. Keeps track of whether or not the form is valid to submit.
  4. Supplies a function that can be used to call a submit function.

The basic outline of the function looks like this:

<FormContainer fieldDefaults={fieldDefaults} errorFuncs={errorFuncs} onSubmit={onSubmit}>
  {({ fields, errorValues, triggerSubmit, submitDisabled }) => {
    return(...)
  }}
</FormContainer>
Enter fullscreen mode Exit fullscreen mode

So the form takes a set of defaults, a set of functions to validate the fields, and a submit function. The component returns a list of field values, any errors, a function to trigger a submit, and a boolean of whether or not the form is valid. With that, you can structure the form however you want, and it will be easy in the future to rearrange or update the form fields or logic.
The component definition is fairly simple. Setting the state is a little complex, so I’ll explain it in detail.

state = {
  fields: {
    ...Object.keys(this.props.fieldDefaults).reduce((acc, curr) => (
      {
        ...acc,
        [curr]: {
          value: this.props.fieldDefaults[curr],
          isDirty: false,
        },
      }
    ), {}),
  },
  errorFuncs: this.props.errorFuncs,
}
Enter fullscreen mode Exit fullscreen mode

To understand what’s going on here, you’ll need to understand two things. First, the reduce function, which you can read up on here. Second, object destructuring, which you can learn about here.Â

Basically, this sets the initial state of the form. The container is sent in an object with key-value pairs of the name of the field and the initial value of that field. This function creates an object with the key ‘field’ with an object for each field inside. Each field object has a value (which the container is given) and an initial ‘isDirty’ value (false). The ‘isDirty’ value is used so the container knows if the field has been changed by the user yet, so no errors will show up before then. After the function runs, the state might looks something like this:

{
  fields: {
    firstName: {
      value: '',
      isDirty: false,
    },
    lastName: {
      value: '',
      isDirty: false,
    },
    email: {
      value: '',
      isDirty: false,
    },
  },
  errorFuncs: { ... }
}
Enter fullscreen mode Exit fullscreen mode

The component formats the data it will send back, and sends it through by rendering its children with parameters:

return (
  this.props.children({
    fields, errorValues, onChange, triggerSubmit, submitDisabled
  })
);
Enter fullscreen mode Exit fullscreen mode

The onChange function sets a new field value in the state, and sets the ‘isDirty’ field to true.

Solving React forms this way gives you total control over how the form is displayed, but you still get validation, errors, and all the benefits of a controlled form. We’ve been using this component for our forms for a little while now, and I’ve liked the simplicity and consistency of it.
Anything you would have done differently? Questions about how the above works? Let me know, I’m always looking to improve!

💖 💪 🙅 🚩
derekbrimley
Derek Brimley

Posted on November 4, 2017

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related