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:
- Sets the default values for each field, and keeps track of any changes, and if they have been touched.
- Returns an object with error messages.
- Keeps track of whether or not the form is valid to submit.
- 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>
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,
}
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: { ... }
}
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
})
);
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!
Top comments (1)
Some have labeled this an anti-pattern, but others have argued that it is more capable than normal, boring old higher-order components.
They argue for different things.
One says that function as a child component is bad, but using another prop for the function is okay.
The other argues that a function prop is better than a higher order component.