redux-inputs: A library for taming forms in React

Forms are critical parts of any interactive experience on the web. Web applications must not only display content, but respond to user interaction. From login, to address entry, to sorting and filtering controls, to wizard-like multi-page flows, the web couldn’t exist without the form. But what’s the right way to make a form?
The first option is a basic html form. They look like
<form action=”/submit.php”> <input type="submit">Go!</input> </form>
They are simple, but lack responsiveness. Validation is generally slow, as it requires the whole form to submit and round-trip to the server. HTML does provide us with the ‘pattern’ attribute on inputs, but a regex does not usually provide enough flexibility to implement great validation. Values are sent in the form-urlencoded format, which uses strings for all values and cannot represent all the types present in JSON. The string “500” and the number 500 are collapsed into the same “500”. Forms without javascript are unable to provide the interactivity expected in modern websites. To achieve this, the browser needs to do more.
At Zillow, we created a way of dealing with more advanced forms using YUI, with the following configuration available on each input:
{ id, formatter parser, validator defaultValue }
State was kept in the DOM, in the input elements themselves. A delegate would get attached to the form to listen for blur events from each tracked id. On blur, the value would be read, run through the parser function, then the validator function. If the value was invalid, an error class would be applied to the input and the entered value kept. If the value was valid, it would be run through the formatter and that value put back into the dom. When the form was to be submitted, the values had to be collected back out of the DOM then run through the parser and validator. Since our APIs take JSON inputs, an API request was then created and posted in JSON.
This worked well for the most part. We were able to add on asynchronous validation for logic that is best kept on the server (like a list of valid zip codes). We could also enable automatic updating when the user had stopped typing for an amount of time.
Where this approach became hairy was derived state. A change in a form input could filter out types of results, update a calculation, or show a different branch of questions in a multi-page flow. While the current characters being typed made sense to store in the DOM, other parts of the application couldn’t go and look at it directly. Since they needed to know when changes occurred, they had to listen to the change events the form was firing and keep their own state of that last known value. This littered state all over the application with fragile links to keep updated, and no clear way of seeing the chain of events that caused a particular value to be what it was.
Management of state over time is one of the problems React is great at solving. When we tried React, it delivered on that promise. Thinking carefully about where each bit of state belonged encouraged better overall design. However, dealing with forms in React is one of the more challenging aspects of using the library. We iterated through a couple of solutions to handle the problem, and we’ve found an approach that works for all the forms we can think up.
redux-inputs represents our attempt at solving ‘Forms in React’. At a high level, a form is modeled as a map of input names to values, stored in a redux store. The values are logical, parsed, validated values like numbers, booleans, or strings. In order to change these values, you hook up a dispatchChange function to the onChange of a wrapped input. Inputs are treated as controlled, but you can use something like BlurInput to only send onChange events when the field is blurred.
Each input is configured as follows:
{ id, defaultValue, validator }
As parsing and formatting have nothing to do with logical values, they are given as props to a wrapped input, and are called automatically when an input’s value changes. The inputs config is just a plain object, allowing extension with new functionality. One experiment we are trying is putting api validation errors in the input configuration. On some pages, each input has its own type of validation error from the API. When that error is received, that particular input can be put into an error state.
Using redux for state management confers many benefits discussed elsewhere, but two features are particularly useful for redux-inputs forms: being able to track each action and associated update in redux devtools, and connecting any component that needs to know the form values to get the state required. For example, https://www.zillow.com/mortgage-calculator/ takes the inputs changed on the left and creates a link that includes those values under ‘Full Report’. Being able to take state from forms and easily use it in other places allows for easier creation of great user experiences.
What does a form look like when built with redux-inputs?
const inputsConfig = { email: { defaultValue: 'test@example.com', validator: (value) => typeof value === 'string' && value.indexOf('@') >= 0 } };
Starting off, we define the state of a form through a plain object known as the inputsConfig. Each key is the name of an input, and the value is a configuration object with the spec defined above. In this case, we define a single input, ‘email’, with a default value of ‘test@example.com’, with a validation function that returns true only if the given value is a string and the string contains an ‘@’ sign. When an input changes, this validator function will be called. If the validator function returns false, the input will be put into an error state.
const reducer = combineReducers({ inputs: createInputsReducer(inputsConfig) }); const store = createStore(reducer, applyMiddleware(thunk));
Next, we define our reducer by using the ‘createInputsReducer’ function. It takes the inputsConfig from above. In this case, we mount the inputs reducer under the key ‘inputs’ in our redux store. This is the default location, but can be overridden if you wish (e.g. to mount multiple forms in one store). The store is created with this reducer and the thunk middleware is added.
const Field = ({id, value, error, onChange, errorText}) => ( <div> <input name={id} value={value} onChange={(e) => onChange(e.target.value)}/> {error ? <p style={{color: 'red'}}>{errorText}</p> : null} </div> ); const ReduxInputsField = ReduxInputsWrapper(Field);
Here we create a functional React component named Field that uses the set of props provided by redux-inputs: id, value, error, errorText, and onChange. It renders a simple controlled input with id, value and onChange, and adds red errorText to appear when the error prop is true. If you use something like material-ui or an in-house component library, you would render that here instead. Lastly, we wrap the Field in the higher order component ReduxInputsWrapper so that the correct props are provided. ReduxInputsWrapper also enables features like setting a parser and formatter function, though they are not used in this example.
const Form = ({ inputs, reduxInputs }) => ( <form> <ReduxInputsField errorText="Your email must contain an @" {...reduxInputs.inputProps.email}/> <h3>Input state</h3> <pre>{JSON.stringify(inputs, null, 2)}</pre> </form> );
Now, all we need to do to complete our form is get the right props to pass to the ReduxInputsField. In this case, these are located at props.reduxInputs.inputProps.email. By using the jsx spread operator, we are able to easily provide all of these props to the ReduxInputsField at the top level. Lastly, we display the contents of the ‘inputs’ state object just to show what state that redux-inputs is storing.
const FormContainer = connectWithInputs(inputsConfig)(s => s)(Form);
To create FormContainer, we use redux-input’s connectWithInputs function and pass it the inputsConfig from above. This returns us a function that acts like react-redux’s connect. We pass all state through as-is to the component with the ‘s => s’ function, and then provide the Form component to be wrapped.
ReactDOM.render(<Provider store={store}><FormContainer /></Provider>, document.getElementById('container'));
Finally, we render our FormContainer to the page, wrapped by a react-redux Provider. For a running version of this example, look at https://zillow.github.io/redux-inputs/examples/basic/.
Putting it all together including imports,
import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, combineReducers, applyMiddleware } from 'redux'; import { createInputsReducer, connectWithInputs, ReduxInputsWrapper } from 'redux-inputs'; import { Provider } from 'react-redux'; import thunk = from 'redux-thunk'; const inputsConfig = { email: { defaultValue: 'test@example.com', validator: (value) => typeof value === 'string' && value.indexOf('@') >= 0 } }; const reducer = combineReducers({ inputs: createInputsReducer(inputsConfig) }); const store = createStore(reducer, applyMiddleware(thunk)); const Field = ({id, value, error, onChange, errorText}) => ( <div> <input name={id} value={value} onChange={(e) => onChange(e.target.value)}/> {error ? <p style={{color: 'red'}}>{errorText}</p> : null} </div> ); const ReduxInputsField = ReduxInputsWrapper(Field); const Form = ({ inputs, reduxInputs }) => ( <form> <ReduxInputsField errorText="Your email must contain an @" {...reduxInputs.inputProps.email}/> <h3>Input state</h3> <pre>{JSON.stringify(inputs, null, 2)}</pre> </form> ); const FormContainer = connectWithInputs(inputsConfig)(s => s)(Form); ReactDOM.render(<Provider store={store}><FormContainer /></Provider>, document.getElementById('container'));
For more features and examples, check out https://github.com/zillow/redux-inputs.
Why yet another javascript library? Redux-form is a popular library that also helps to manage form state in react with redux. Simply, redux-form did not exist when we started working with React. By the time it did exist, we had a library that was solving our problem and that allowed us to iterate quickly on it. Additionally, there is a difference in design philosophies. Redux-form is mainly focused on forms as a whole, validating and submitting all at once, while redux-inputs provides validation focused on individual fields, providing a helper to validate a whole form at once.