Writing maintainable forms with redux

Simon Boudrias
3 min readDec 23, 2016

--

Writing forms is often a major part of most UI engineers works. Whether it’s a series of input fields, or dynamic controls, a form is the basic concept behind any data manipulation.

A lot of libraries are out there proposing solutions to help organize forms. But what you want might just be a simple recipe to keep forms simple and organized.

So what are the base requirements for an usable form?

  1. Needs validation
  2. Load pre-existing data (pre-fill or edit)
  3. Keep track of changes

Form Reducer

First step is creating a reducer to store our form. We basically want to be able to update the form with new values and reset it once we’re done.

const UPDATE_PERSON_FORM = 'UPDATE_PERSON_FORM';
const RESET_PERSON_FORM = 'RESET_PERSON_FORM';
const updatePersonForm = (form) => ({
type: UPDATE_PERSON_FORM,
form
});
const resetPersonForm = () => {type: RESET_PERSON_FORM};const PersonFormReducer = (state={}, action) => {
switch (action.type) {
case UPDATE_PERSON_FORM:
return {
...state,
...action.form,
};
case RESET_PERSON_FORM:
return {};
default:
return state;
}
};

The reducer should stay simple, the magic will happens in selectors.

Selectors

To me, selectors are a core concept of redux. You might be using reselect, but simple functions work as well.

The first selector we need is one that’ll select the form values. This entails handling default values, merging pre-existing data and potentially imposing bounds on values.

const personFormSelector = createSelector([
(state) => state.personForm,
(state, id) => state.person.by_id[id],
], (form, person) => {
return {
...person || {},
...form,
};
});

This selector is assuming the form and the models keys are named the same. If that’s not your case, the form selector is a good place to handle name mapping. It’s also a good place to enforce values boundaries/filtering.

const personFormSelector = createSelector([
(state) => state.personForm,
(state, id) => state.person.by_id[id],
], (form, person) => {
var formValues = {
firstName: '',
...person || {},
...form,
};
// Impose lower bound on age
formValues.age = Math.max(13, parseInt(formValues.age));
return formValues;
});

This selector should contains all the logic necessary to prefill the form with default values and existing entity values, enforce validation and filtering. Separating final values computation from the redux reducer is useful to make your system more robust towards bad values you might have stored in your database in the past, or to be resilient to bad/wrong code updating the store itself.

Next step, we need a selector handling validation.

const personFormErrorsSelector = createSelector([
personFormSelector,
], (form) => {
var errors = {};
if (form.firstName.length > 50) {
errors.firstName = 'First name cannot contain more than 50 characters';
}
if (!isValidEmail(form.email)) {
errors.email = 'Please enter a valid email';
}
return errors;
});

Take the form as an argument, and check if values are valid. If they’re not, fill the error object with error messages.

Binding to the view

We’ll use react for the view layer, but this pattern is generic enough that it could work for many different frameworks.

class PersonForm extends React.Component {
onChange(e) {
this.props.updatePersonForm({
[e.target.name]: e.target.value,
});
}
onSubmit() {
// If there's no error, the object is empty
if (Object.keys(this.props.errors).length > 0) {
alert('Please fix the errors before submitting');
return;
}
this.props.onSubmit(this.props.form);
}

render() {
var {form, errors} = this.props;
return (
<form onSubmit={this.onSubmit}>
<label>
First Name
<input name="firstName" value={form.firstName} onChange={this.onChange}/>
{errors.firstName}
</label>
<label>
Last Name
<input name="email" value={form.email} onChange={this.onChange}/>
{errors.email}
</label>
</form>
);
}
}
export default connect(
(state, {id}) => ({
form: personFormSelector(state, id),
errors: personFormErrorsSelector(state),
}),
{updatePersonForm}
)(PersonForm);

Using react-redux, we bind our view to our form object. Then we only need to use it:

<PersonForm id={editedPersonId} onSubmit={savePersonForm}/>

Wrapping up

So really that’s how there really is to it. This basic architecture is extremely flexible and extendable; but it keeps parts of logic well defined and separated.

Want to display a loader while you’re sending data through ajax? Return a promise from onSubmit and display a loader until success or failure.

Want to have different levels of form errors? Add a personFormWarningSelector.

My team’s been using this pattern for handling form with redux for about a year on production. So far it provided us enough flexibility to handle most edge cases our UI might require. It also keep the barrier of entry lower for new developers joining our team given there’s no extra library own API to learn to get up and running working with forms.

I hope you’ll find this recipe useful too. Let me know!

--

--

Simon Boudrias

Software engineer working at Yelp. Core contributor of Yeoman. Montreal ✈︎ San Francisco ✈︎ Beijing (soon!)