Using Redux Form to handle user input

This post is part of a series on my learnings while building a React app.

Most user input in a web app will come through HTML forms. And that input is, presumably, changing something in your application state. In this series of posts, we’re talking about using Redux to manage application state. So it would be nice to easily connect HTML forms with Redux.

That’s exactly what the redux-form package does.

I have to admit to waffling quite a bit on whether to use redux-form. At first, I was excited to find a package that seemed to address exactly the need I had. Then I started working with redux-form and react-boostrap at the same time, got myself all tied up in knots, and decided to write my own code to get HTML form input into Redux. After gaining a little more experience, however, I realized the problem was with me, not redux-form.

TL;DR: I recommend redux-form if you are using redux. But understand going in that it has the usual trade-offs of a package designed to be used in a wide variety of apps. It can probably do what you need. It almost certainly has functionality you don’t need. That means additional complexity and time to learn how to use it to solve your specific problems.

The two pieces of redux-form that you interact with most are:

  • The reduxForm() decorator function. This works similarly to react-redux’s connect() function — you wrap your component with it to enable redux-form functionality. Typically, the component you are wrapping is the one that contains the HTML <form> element. I’ll refer to this wrapped component as “the form component.”
  • Field and FieldArray components. You add Field and FieldArray as descendants of the form component. These, in turn, wrap the standard HTML input elements like <input> and <select>.

Using redux-form with custom components

You don’t have to use redux-form with the standard HTML input elements. You can use your own custom components.

This is where I initially got myself quite confused. I am using react-bootstrap, so I already had a custom component that contained the react-bootstrap boilerplate — FormGroup, FormControl, Label, HelpBlock, etc. — so that all of my inputs looked consistent.

Initially, I tried to insert redux-form’s Field as a child of react-bootstrap’s FormControl. Save yourself some pain: if you are using these two packages together, the redux-form Field component goes on the outside, and the react-bootstrap FormControl component goes on the inside!

Overall, my rendered component tree looks like this:

1. reduxForm() wrapper component from redux-form.
2. My form component (with HTML <form> element).
3. Field component from redux-form
4. My component with react-bootstrap boilerplate.
5. FormControl component from react-bootstrap.
6. HTML <input> element.

Of course, there are multiple Fields, so 3–6 are repeated multiple times within the form component.

The redux-form Field component

The Field component will pass several important props to a custom component:

  • name. The same name prop you gave to the Field component.
  • input. An object containing all of the props that Field would have passed to a standard HTML input element: name, onChange and other event handlers and value. The value prop is what turns the input into a controlled component.
  • meta. An object containing status information about the field: has it been touched, is it pristine or dirty, validation error messages, etc.

Field also passes along any other props you give it. This is all well-described in the documentation.

The reduxForm() decorator

The reduxForm() decorator will pass a bunch of event handlers to your form component as props — most importantly, handleSubmit. You usually set your form’s onSubmit prop to handleSubmit.

In addition, you are also expected to pass your own onSubmit function as a prop to the reduxForm()-wrapped form component.

Huh?

The idea is that when the form is submitted — by clicking a button, by hitting enter, by javascript, by whatever means — redux-form calls its own handleSubmit function. That function performs any validation you have configured it to perform.

If and only if the form is valid does handleSubmit call your onSubmit function, passing it an object containing the form’s current values.

React's form onSubmit calls:
redux-form’s handleSubmit, which (if the form is valid) calls:
the function you pass in as a prop named onSubmit

Let’s assume that you want a Redux action dispatched when your form is submitted. Taking all of the above into account, your form component will look something like this:

class MyForm extends React.Component {
// this.props.handleSubmit is created by reduxForm()
// if the form is valid, it will call this.props.onSubmit,
// which I added below in the connect() function.
const { handleSubmit } = this.props
render() {
<form onSubmit={handleSubmit}>
<Field name='user.email' component='input' type='email' />
<Field name='user.name' component='input' />
...
<input type='submit' value='Save' />
</form>
}
}
// Your component is wrapped by redux-form
// with the configuration you specify
const myReduxForm = reduxForm({
form: ‘myFormName’, // required by reduxForm()
warn: (values, props) => { ... }, // optional
error: (values, props) => { ... } // optional
})(MyForm)
// Your redux-form-wrapped component is wrapped by react-redux.
export default connect(
state => ({
// optional. grab values to fill the form from somewhere.
initialValues: state.foo.bar
}),
dispatch => ({
// reduxForm() expects the component to have an onSubmit
// prop. You could also pass this from a parent component.
// I want to dispatch a redux action.
onSubmit: data => dispatch(myActionToDoStuff(data))
})
)(myReduxForm)

Where is the form data stored?

The one additional piece of redux-form you need to configure is its reducer, which you do in your reducer setup:

import { reducer as ‘formReducer’ } from ‘redux-form’
...
export default combineReducers({
// other reducers,
form: formReducer // must be named 'form'
})

While your form component is mounted, the form reducer’s state will have a top-level value with the same name you passed to reduxForm(). With my sample component above, the state will be in an object located at state.form.myFormName.

Inside that object will be a bunch of data that redux-form uses to track the state of your form: initial and current field values, validation status for each field, whether a field has been touched or changed from the initial values, etc.

Notice that the form reducer contains the initial and current value of all fields. This is important to understand: redux-form connects your form and field values to its own reducer’s state — not to yours.

If you want to update state in one of your reducers, you will need to do what I did in the sample code above: in your onSubmit function, dispatch an action that some other reducer(s) can handle. In many cases, you are going to send the form data to an API, so the action is likely to be asynchronous. (See post on async actions.)

Does this violate the principle of keeping state in exactly one place? Technically, maybe. But I think you can reasonably split that hair: the state in redux-form’s reducer is the ephemeral state of the form UI. Once the form is validated and submitted, the data becomes “real” and you update the state in your reducers.

In my app, onSubmit dispatches an async action that calls an API, and only after the async action resolves — that is, after the data has successfully been sent to the server — do I dispatch a SAVE_SUCCEEDED action, which updates my reducer’s state.

That works for me, and I like that there is no direct connection between redux-form and my state — I control when and how my state changes based on actions dispatched from form event handlers. And if I ever want to move away from redux-form, it will be much, much easier.

You don’t have to follow this pattern, but it is the most straightforward. Redux-form allows you to customize where the form state is stored. Your reducer can listen directly for redux-form’s FORM_SUBMITTED or other actions. And there are many other ways to customize it, if that’s what you need.

Validation with redux-form

If you read the form component sample closely, you’ll see I passed two values to reduxForm() that we haven’t discussed yet: warn and error. These are validation functions.

Returning validation error messages from the error function will prevent redux-form from submitting your form. That is, handleSubmit will not call your onSubmit as long as the error function returns something.

By contrast, returning validation warnings from warn does not prevent form submission. Instead, it simply makes the warning message(s) available to your form and field components (the ones wrapped by reduxForm() and Field, respectively). That allows you to show the warnings, but still let the user submit the form.

I use warnings a lot in the app I am building. I often need to let the user save partially completed work to the server while it is still invalid for use elsewhere in the app. Similar to saving a draft email with spelling mistakes or an empty subject line.

Values — nested or flat

Another detail you might have noticed in the form component sample is the names I passed to the two Field components: user.email and user.name.

In the redux-form reducer, the initial and current values of your form are stored as two separate objects. You can either use a flat object or a nested object. A flat object would look like this:

{
email: 'askywalker@deathstar.com',
name: 'anakin'
side: 'dark',
aliases: ['darth vader', 'sith lord'],
}

All of the values are in top-level properties of the object. Alternatively, you can use a nested object like:

{
user: {
email: 'askywalker@deathstar.com',
name: 'anakin',
side: 'dark',
aliases: ['darth vader', 'sith lord'],
children: {
luke: {
name: 'luke',
planet: 'tatooine'
},
leia: {
name: 'leia',
planet: 'alderan'
}
}
},
}

In the flat case, your Field names would be email and name. In the nested case, your Field names would be dotted paths to each value: user.email, user.name, user.children.leia.planet, etc. (In either case, you can work with arrays like aliases with redux-form’s FieldArray component or by indexing directly into them with a Field name like user.aliases[1].)

Which should you use, flat or nested? It’s entirely up to you, and you can mix and match as you see fit. Just be aware that all of the following must be aligned in terms of object shape:

  • The initialValues prop you pass to reduxForm().
  • The names (or dotted paths) you give to Field components.
  • The values object passed to your onSubmit function.
  • The values object passed to your warn and error validation functions — and the objects you return from those functions.

More on validation

That last bit was unexpected (by me, anyway). Each validation function returns an object containing the warning/error messages for each value that fails validation. Returning an empty object {} means all fields passed validation.

In the flat case, an object with validation errors might be:

{
side: 'The dark side of the Force is not allowed.'
}

In the nested case, it might be:

{
user: {
side: 'The dark side of the Force is not allowed.'
children: {
leia: {
planet: '"alderan" is not a planet. Please check the
spelling and try again'
}
}
}
}

The values you return from your error and warn validation functions must have the same shape as the form’s values, or redux-form won’t know how to connect the error/warning message and validation status with the corresponding Field.

Performance with large forms

You need to be careful about performance when a form has many fields. The major API change between redux-form 5.x and 6.x was motivated in large part by the need to improve performance of large forms.

Even with redux-form 6.x I have seen slow-downs in forms with just a few fields (in development). Redux-form creates controlled form inputs. It also tracks a lot of information about the form and each field. Every focus/change/blur, validation state change, etc. dispatches an action and results in some modification of the redux-form state. If you aren’t careful, this can cause your entire form to re-render very, very often.

I was able to resolve the slow-downs I encountered by making some components PureComponents (meaning they only re-render if a top-level prop or state value has changed) and in other cases by implementing shouldComponentUpdate() to limit when updates occur. This is normal in large React applications — but you might need to do it a bit sooner when using redux-form than you would otherwise.

If you’re going to do something in a field’s onChange, you should seriously consider debouncing so that it only happens after a pause in the user’s typing.

Conclusion

Redux-form can be complicated. It will handle all the typical use cases, but it may take you a while to figure out exactly how it works.

You may need to prevent components from updating, with PureComponent or shouldComponentUpdate(), earlier in your development process than you might otherwise.

I haven’t even touched on FieldArray, async (server-side) validation, normalizing of values, or other more advanced uses.

Taking all of that into account, now that I have (I think) grokked redux-form, it is helping me get stuff done and I recommend it.