Handling the data and events related to forms and input fields in web apps is not most peopleās idea of fun. Thankfully though, there are plenty of available libraries that abstract away some of the tedious work. If your React app already happens to use Redux for state management, then Redux Form is one such option. It makes managing your appās form state easy and straightforward.
Letās go over just enough of the basics to get you up and running creating and handling form data.
š Alligator.io recommends ⤵
Fullstack Advanced React & GraphQL by Wes BosInstallation & Setup
Go ahead and add the redux-form package to your project:
$ yarn add redux-form
# or, using npm:
$ npm install redux-form
Then youāll add a reducer named form that points to Redux Formās reducer. Thatāll probably mean that your app will have more than one reducer, so you can use something like the following with the combineReducers function to create a root reducer with all your appās reducers:
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import todoReducer from './reducers';
import App from './App';
const rootReducer = combineReducers({
todo: todoReducer,
form: formReducer
});
const store = createStore(rootReducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
Note that the name of the form reducer is important and should be kept to form.
And with this, youāre ready to start defining forms and have their state entirely managed by Redux Form.
Usage
Letās build a simple form that weāll call GatorForm step by step. As youāll see, weāll make use of two exports from Redux Form: the reduxForm function and the Field component. First, just an input field with its label and a submit button:
components/GatorForm.js
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const GatorForm = ({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit(val => console.log(val))}>
<label htmlFor="first-name">Your first name:</label>
<Field
name="firstName"
type="text"
component="input"
id="first-name"
placeholder="Benedict"
/>
<button type="submit">Submit</button>
</form>
);
};
export default reduxForm({
form: 'gatorForm'
})(GatorForm);
There isnāt much to our form so far, but we can already draw some lessons:
- The reduxForm function is used to return a higher-order component that is augmented will all kinds of props that relate to our form. It expects a config object with at least a unique form name.
- We make use of one of the callback props available on the HOC, handleSubmit. It can be used like in our example with a callback function that will be called when the form is submitted and that will receive the form values. It can also be used without the callback,
onSubmit={handleSubmit}
, provided that the component that uses the form has anonSubmit
callback prop, which will be called. The form wonāt be submitted if validation fails. - The Field component is used to add an input field. Here we used the input component of type text. All regular form fields are available. For example, for an input component, the type could have been number, email, password, radio or checkbox. The component prop also accepts the textarea and select string values as well as custom components.
- The name given to a field is important because itāll be used when accessing the formās values. Here for example our field value will come up under firstName.
There are a ton more props available from the HOC, notable ones include warning, pure, pristine, reset, error, anyTouched and dirty.
Letās add a little bit to our form and learn a little bit more about the available props at the same time:
components/GatorForm.js
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const foods = ['pizza', 'tacos', 'nachos', 'hot dogs'];
const GatorForm = ({ handleSubmit, reset, pristine, submitting, valid }) => {
return (
<form onSubmit={handleSubmit(val => console.log(val))}>
<label htmlFor="first-name">Your first name:</label>
<Field
name="firstName"
type="text"
component="input"
id="first-name"
placeholder="Benedict"
/>
<label htmlFor="food-choice">Food choice</label>
<Field name="foodChoice" id="food-choice" component="select">
<option />
{foods.map(food => {
return (
<option key={food} value={food}>
{food}
</option>
);
})}
</Field>
<button type="submit" disabled={!valid || pristine || submitting}>
Submit
</button>
<button type="button" onClick={reset}>
reset
</button>
</form>
);
};
export default reduxForm({
form: 'gatorForm'
})(GatorForm);
Now our form can be reset and the submit button will be disabled when the form is not valid, when itās pristine or when itās in the process of submitting. We also added a select field with a few options populated from an array of available options.
Custom Field Components
For maximum flexibility, itās just as easy to provide custom components to the Field component. This becomes especially useful to add additional markup for validation messages. Below we use a function that returns a component that includes an input field and markup for eventual validation errors:
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const newField = ({
input,
type,
placeholder,
id,
meta: { touched, error }
}) => {
return (
<div>
<input {...input} placeholder={placeholder} type={type} id={id} />
{touched && error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
};
const GatorForm = ({ handleSubmit, reset, pristine, submitting, valid }) => {
return (
<form onSubmit={handleSubmit(val => console.log(val))}>
<label htmlFor="first-name">Your first name:</label>
<Field
name="firstName"
type="text"
component={newField}
id="first-name"
placeholder="Benedict"
/>
<label htmlFor="email">Email:</label>
<Field
name="email"
type="email"
component={newField}
id="email"
placeholder="benedict@alligator.io"
/>
<button type="submit" disabled={!valid || pristine || submitting}>
Submit
</button>
<button type="button" onClick={reset}>
reset
</button>
</form>
);
};
export default reduxForm({
form: 'gatorForm'
})(GatorForm);
As you can see, the function that returns the component receives a bunch of useful props. Itās important here to spread the input props ({...input}
) so that the input field receives all the event handling callback props. We can also access the other static props used with the Field component.
As you can also see, weāve set the stage for validation and displaying error messages. We first check that the field has been touched (focused in and out) to ensure that no error message is shown on an untouched input field.
Validation
One of the most important tasks when it comes to form handling on the front-end is proper validation so that the UX can feel intuitive and users are properly guided towards correctly filling the form. Thankfully, Redux Form also makes validation easy and supports both synchronous and asynchronous validation. Here weāll have a look at how to perform synchronous validation.
Hereās how youād perform validation on the form level:
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const newField = ({
input,
type,
placeholder,
id,
meta: { touched, error },
...rest
}) => {
return (
<div>
<input {...input} placeholder={placeholder} type={type} id={id} />
{touched && error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
};
const GatorForm = ({ handleSubmit, reset, pristine, submitting, valid }) => {
return (
<form onSubmit={handleSubmit(val => console.log(val))}>
<label htmlFor="first-name">Your first name:</label>
<Field
name="firstName"
type="text"
component={newField}
id="first-name"
placeholder="Benedict"
/>
<label htmlFor="email">Email:</label>
<Field
name="email"
type="email"
component={newField}
id="email"
placeholder="benedict@alligator.io"
/>
<button type="submit" disabled={!valid || pristine || submitting}>
Submit
</button>
<button type="button" onClick={reset}>
reset
</button>
</form>
);
};
const myValidator = values => {
const errors = {};
if (!values.firstName) {
errors.firstName = 'First name is required';
} else if (values.firstName.length < 3) {
errors.firstName = "Your name can't be that short!";
}
if (!values.email) {
errors.email = 'Hold on a minute, we need an email!';
} else if (!/(.+)@(.+){2,}\.(.+){2,}/i.test(values.email)) {
// use a more robust RegEx in real-life scenarios
errors.email = 'Valid email please!';
}
return errors;
};
export default reduxForm({
form: 'gatorForm',
validate: myValidator
})(GatorForm);
You pass a validator function under the validate key of the reduxForm function. That function receive the form values and should return an empty object for a valid form or an object with field names and an error messages when there are validation errors. As you saw previously, itās then easy in a custom field component to access and display error messages.
Similarly, you can perform validation on the field level by using a validate prop on the Field and passing individual validation functions that receive the current value. Return undefined when thereās no error or an error message string when thereās an error:
import React from 'react';
import { reduxForm, Field } from 'redux-form';
const newField = ({
input,
type,
placeholder,
id,
meta: { touched, error },
...rest
}) => {
return (
<div>
<input {...input} placeholder={placeholder} type={type} id={id} />
{touched && error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
};
const required = value => (value ? undefined : 'Required!');
const longEnough = value =>
value && value.length >= 3 ? undefined : 'Too short!';
const email = value =>
value && /(.+)@(.+){2,}\.(.+){2,}/i.test(value)
? undefined
: 'Invalid email!';
const GatorForm = ({ handleSubmit, reset, pristine, submitting, valid }) => {
return (
<form onSubmit={handleSubmit(val => console.log(val))}>
<label htmlFor="first-name">Your first name:</label>
<Field
name="firstName"
type="text"
component={newField}
id="first-name"
placeholder="Benedict"
validate={[required, longEnough]}
/>
<label htmlFor="email">Email:</label>
<Field
name="email"
type="email"
component={newField}
id="email"
placeholder="benedict@alligator.io"
validate={[required, email]}
/>
<button type="submit" disabled={!valid || pristine || submitting}>
Submit
</button>
<button type="button" onClick={reset}>
reset
</button>
</form>
);
};
export default reduxForm({
form: 'gatorForm'
})(GatorForm);
āµ May the winds of form handling always be at your back! Refer to the API docs to dig deeper into the possibilities.