We look at basic uses of Redux form.
Form validation is a frequently needed feature in web apps. React does not come with its own form validation since it’s supposed to be a view library that provides developers with code structure. Fortunately, developers have come up with many form validation solutions. One of them is Redux Form, located at https://redux-form.com/8.2.2/.
As its name suggests, Redux Form uses Redux for storing the form data. It requires its own reducer for storing a form’s data. The form component that you write connects to the built-in reducer to store the data. If you want to get initial data, you load the data in your Redux store, then Redux Form can retrieve it.
Form value checks and form validations errors are provided by you. You can get form values from the store when you need it. The form input components can be customized easily by passing our custom component into the Field
component provided by Redux Form.
In this article, we will write an address book app to illustrate the use of Redux Form for form validation. We need will have a Redux store for storing the form data, the selected contact data when editing, and a list of contacts. To start, we will run Create React App by running npx create-react-app address-book
to create the app.
After that is run, we add our own libraries. We need Axios for making HTTP requests, React Bootstrap for styling, React Redux and Redux for state management, React Router for routing, and of course, Redux Form for form validation.
We install them by running:
npm i axios react-bootstrap react-redux react-router-fom redux redux-form
With our libraries installed, we can start writing our app. We create all files in the src
folder unless otherwise specified. To start, we create actionCreators.js
in the src
folder and add:
import { SET_CONTACTS, LOAD } from "./actions";
const setContacts = contacts => {
return {
type: SET_CONTACTS,
payload: contacts
};
};
const setCurrentContact = contact => {
return {
type: LOAD,
data: contact
};
};
export { setContacts, setCurrentContact };
These are the actions and payload that we dispatch to our Redux store. setContacts
is for setting a list of contacts and setCurrentContact
is for loading the contact for editing.
Next, we create actions.js
and add:
const SET_CONTACTS = "SET_CONTACTS";
const LOAD = "LOAD";
export { SET_CONTACTS, LOAD };
These are the constants for the action types of our store dispatch actions. Next in App.js
, we replace the existing code with:
import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();
function App() {
return (
<div className="App">
<Router history={history}>
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">Address Book App</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/">Home</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<Route path="/" exact component={HomePage} />
</Router>
</div>
);
}
export default App;
to add the React Bootstrap Navbar
component to the top of our page. We also add the React Router route for the home page that we’ll create.
Next, we create ContactForm.js
and add:
import React from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";
import { connect } from "react-redux";
import { setContacts, setCurrentContact } from "./actionCreators";
import { Field, reduxForm } from "redux-form";
import { renderInputField } from "./RenderInputField";
import { renderCountryField } from "./RenderCountryField";
import { getFormValues, isInvalid } from "redux-form";
const validate = values => {
const errors = {};
if (!values.firstName) {
errors.firstName = "Required";
}
if (!values.lastName) {
errors.lastName = "Required";
}
if (!values.city) {
errors.city = "Required";
}
if (!values.address) {
errors.address = "Required";
}
if (!values.region) {
errors.region = "Required";
}
if (!values.postalCode) {
errors.postalCode = "Required";
} else {
if (
(values.country == "United States" &&
!/^[0-9]{5}(?:-[0-9]{4})?$/.test(values.postalCode)) ||
(values.country == "Canada" &&
!/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/.test(values.postalCode))
) {
errors.postalCode = "Invalid postal code";
}
}
if (!values.phone) {
errors.phone = "Required";
} else {
if (
(values.country == "United States" || values.country == "Canada") &&
!/^[2-9]d{2}[2-9]d{2}d{4}$/.test(values.phone)
) {
errors.phone = "Invalid phone";
}
}
if (!/[^@]+@[^.]+..+/.test(values.email)) {
errors.email = "Invalid email";
}
if (Number.isNaN(+values.age) || values.age < 0 || values.age > 200) {
errors.age = "Age must be between 0 and 200";
}
if (!values.country) {
errors.country = "Required";
}
return errors;
};
function ContactForm({
edit,
onSave,
setContacts,
contact,
onCancelAdd,
onCancelEdit,
invalid,
values,
currentContact,
...props
}) {
const handleSubmit = async event => {
if (invalid) {
return;
}
if (!edit) {
await addContact(values);
} else {
await editContact(values);
}
const response = await getContacts();
setContacts(response.data);
onSave();
};
return (
<div className="form">
<Form noValidate onSubmit={props.handleSubmit(handleSubmit.bind(this))}>
<Field
name="firstName"
type="text"
component={renderInputField}
label="First Name"
/>
<Field
name="lastName"
type="text"
component={renderInputField}
label="Last Name"
/>
<Field
name="address"
type="text"
component={renderInputField}
label="Address"
/>
<Field
name="city"
type="text"
component={renderInputField}
label="City"
/>
<Field
name="region"
type="text"
component={renderInputField}
label="Region"
/>
<Field name="country" component={renderCountryField} label="Country" />
<Field
name="postalCode"
type="text"
component={renderInputField}
label="Postal Code"
/>
<Field
name="phone"
type="text"
component={renderInputField}
label="Phone"
/>
<Field
name="email"
type="email"
component={renderInputField}
label="Email"
/>
<Field
name="age"
type="text"
component={renderInputField}
label="Age"
/>
<Button type="submit" style={{ marginRight: "10px" }}>
Save
</Button>
<Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
Cancel
</Button>
</Form>
</div>
);
}
ContactForm.propTypes = {
edit: PropTypes.bool,
onSave: PropTypes.func,
onCancelAdd: PropTypes.func,
onCancelEdit: PropTypes.func,
contact: PropTypes.object
};
const mapStateToProps = state => {
return {
contacts: state.contacts
};
};
const mapDispatchToProps = dispatch => ({
setContacts: contacts => dispatch(setContacts(contacts))
});
ContactForm = connect(state => ({
values: getFormValues("syncValidation")(state),
invalid: isInvalid("syncValidation")(state)
}))(ContactForm);
ContactForm = connect(
mapStateToProps,
mapDispatchToProps
)(ContactForm);
ContactForm = reduxForm({
form: "syncValidation",
validate,
enableReinitialize: true
})(ContactForm);
ContactForm = connect(state => {
return {
initialValues: state.currentContact.data
};
})(ContactForm);
export default ContactForm;
This is the form for editing and adding our contact. We use Redux Form extensively here. The Form
component is provided by React Bootstrap. The Field
component is from Redux Form. We pass in our custom input field components into the component
prop of the field, along with the name
, type
, and label
of our inputs. We will use them in our custom field components, renderInputField
and renderCountryField
.
In the onSubmit
prop of the Form
component, we wrap our own handleSubmit
function with Redux Form’s handleSubmit
function provided in the props
so that we can use our own form submit handler instead of Redux Form’s handleSubmit
function for handling form submissions.
In our handleSubmit
function, we check for the form’s validity by getting the invalid
prop from the props. The validation rules are in the validate
function at the top of the code, which we passing into the reduxForm
function at the bottom of our code. In the validate
function, we get the form input values in the values
parameter and set the errors
object with the error messages of each field. We can check validation that depends on other fields easily in this function.
Once the form is checked to be valid, whereinvalid
is false
, then we call addContact
or editContact
by passing the value
prop we get from the connect
functions from Redux Form depending on if we are adding or not and save the data. Then we call getContacts
and setContacts
from mapDispatchToProps
to update our Redux store with the latest contact entries from back end. Then call onSave
, which is passed in from HomePage.js
that we will create, to close the modal.
The invalid
, and values
props are generated by running:
ContactForm = connect(state => ({
values: getFormValues("syncValidation")(state),
invalid: isInvalid("syncValidation")(state)
}))(ContactForm);
This block:
ContactForm = reduxForm({
form: "syncValidation",
validate,
enableReinitialize: true
})(ContactForm);
provides us with form validation capabilities of Redux Form in this component, and the initial values of the edit form is retrieved from our store by running:
ContactForm = connect(state => {
return {
initialValues: state.currentContact.data
};
})(ContactForm);
Next, create exports.js
and add:
export const COUNTRIES = [
"Afghanistan",
"Albania",
"Algeria",
"Andorra",
"Angola",
"Anguilla",
"Antigua & Barbuda",
"Argentina",
"Armenia",
"Aruba",
"Australia",
"Austria",
"Azerbaijan",
"Bahamas",
"Bahrain",
"Bangladesh",
"Barbados",
"Belarus",
"Belgium",
"Belize",
"Benin",
"Bermuda",
"Bhutan",
"Bolivia",
"Bosnia & Herzegovina",
"Botswana",
"Brazil",
"British Virgin Islands",
"Brunei",
"Bulgaria",
"Burkina Faso",
"Burundi",
"Cambodia",
"Cameroon",
"Canada",
"Cape Verde",
"Cayman Islands",
"Chad",
"Chile",
"China",
"Colombia",
"Congo",
"Cook Islands",
"Costa Rica",
"Cote D Ivoire",
"Croatia",
"Cruise Ship",
"Cuba",
"Cyprus",
"Czech Republic",
"Denmark",
"Djibouti",
"Dominica",
"Dominican Republic",
"Ecuador",
"Egypt",
"El Salvador",
"Equatorial Guinea",
"Estonia",
"Ethiopia",
"Falkland Islands",
"Faroe Islands",
"Fiji",
"Finland",
"France",
"French Polynesia",
"French West Indies",
"Gabon",
"Gambia",
"Georgia",
"Germany",
"Ghana",
"Gibraltar",
"Greece",
"Greenland",
"Grenada",
"Guam",
"Guatemala",
"Guernsey",
"Guinea",
"Guinea Bissau",
"Guyana",
"Haiti",
"Honduras",
"Hong Kong",
"Hungary",
"Iceland",
"India",
"Indonesia",
"Iran",
"Iraq",
"Ireland",
"Isle of Man",
"Israel",
"Italy",
"Jamaica",
"Japan",
"Jersey",
"Jordan",
"Kazakhstan",
"Kenya",
"Kuwait",
"Kyrgyz Republic",
"Laos",
"Latvia",
"Lebanon",
"Lesotho",
"Liberia",
"Libya",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Macau",
"Macedonia",
"Madagascar",
"Malawi",
"Malaysia",
"Maldives",
"Mali",
"Malta",
"Mauritania",
"Mauritius",
"Mexico",
"Moldova",
"Monaco",
"Mongolia",
"Montenegro",
"Montserrat",
"Morocco",
"Mozambique",
"Namibia",
"Nepal",
"Netherlands",
"Netherlands Antilles",
"New Caledonia",
"New Zealand",
"Nicaragua",
"Niger",
"Nigeria",
"Norway",
"Oman",
"Pakistan",
"Palestine",
"Panama",
"Papua New Guinea",
"Paraguay",
"Peru",
"Philippines",
"Poland",
"Portugal",
"Puerto Rico",
"Qatar",
"Reunion",
"Romania",
"Russia",
"Rwanda",
"Saint Pierre & Miquelon",
"Samoa",
"San Marino",
"Satellite",
"Saudi Arabia",
"Senegal",
"Serbia",
"Seychelles",
"Sierra Leone",
"Singapore",
"Slovakia",
"Slovenia",
"South Africa",
"South Korea",
"Spain",
"Sri Lanka",
"St Kitts & Nevis",
"St Lucia",
"St Vincent",
"St. Lucia",
"Sudan",
"Suriname",
"Swaziland",
"Sweden",
"Switzerland",
"Syria",
"Taiwan",
"Tajikistan",
"Tanzania",
"Thailand",
"Timor L'Este",
"Togo",
"Tonga",
"Trinidad & Tobago",
"Tunisia",
"Turkey",
"Turkmenistan",
"Turks & Caicos",
"Uganda",
"Ukraine",
"United Arab Emirates",
"United Kingdom",
"United States",
"United States Minor Outlying Islands",
"Uruguay",
"Uzbekistan",
"Venezuela",
"Vietnam",
"Virgin Islands (US)",
"Yemen",
"Zambia",
"Zimbabwe"
];
so we get an array of countries for our country field drop down.
Next in HomePage.css
, we add:
.home-page {
padding: 20px;
}
to get our page some padding.
Then we create HomePage.js
and add:
import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { connect } from "react-redux";
import { getContacts, deleteContact } from "./requests";
import { setCurrentContact } from "./actionCreators";
function HomePage({ setCurrentContact }) {
const [openAddModal, setOpenAddModal] = useState(false);
const [openEditModal, setOpenEditModal] = useState(false);
const [initialized, setInitialized] = useState(false);
const [selectedContact, setSelectedContact] = useState({});
const [contacts, setContacts] = useState([]);
const openModal = () => {
setOpenAddModal(true);
};
const closeModal = () => {
setOpenAddModal(false);
setOpenEditModal(false);
getData();
};
const cancelAddModal = () => {
setOpenAddModal(false);
};
const editContact = contact => {
setSelectedContact(contact);
setCurrentContact(contact);
setOpenEditModal(true);
};
const cancelEditModal = () => {
setOpenEditModal(false);
};
const getData = async () => {
const response = await getContacts();
setContacts(response.data);
setInitialized(true);
};
const deleteSelectedContact = async id => {
await deleteContact(id);
getData();
};
useEffect(() => {
if (!initialized) {
getData();
}
});
return (
<div className="home-page">
<h1>Contacts</h1>
<Modal show={openAddModal} onHide={closeModal}>
<Modal.Header closeButton>
<Modal.Title>Add Contact</Modal.Title>
</Modal.Header>
<Modal.Body>
<ContactForm
edit={false}
onSave={closeModal.bind(this)}
onCancelAdd={cancelAddModal}
/>
</Modal.Body>
</Modal>
<Modal show={openEditModal} onHide={closeModal}>
<Modal.Header closeButton>
<Modal.Title>Edit Contact</Modal.Title>
</Modal.Header>
<Modal.Body>
<ContactForm
edit={true}
onSave={closeModal.bind(this)}
contact={selectedContact}
onCancelEdit={cancelEditModal}
/>
</Modal.Body>
</Modal>
<ButtonToolbar onClick={openModal}>
<Button variant="outline-primary">Add Contact</Button>
</ButtonToolbar>
<br />
<div className="table-responsive">
<Table striped bordered hover>
<thead>
<tr>
<th>First Name</th>
<th>Last Name</th>
<th>Address</th>
<th>City</th>
<th>Country</th>
<th>Postal Code</th>
<th>Phone</th>
<th>Email</th>
<th>Age</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{contacts.map(c => (
<tr key={c.id}>
<td>{c.firstName}</td>
<td>{c.lastName}</td>
<td>{c.address}</td>
<td>{c.city}</td>
<td>{c.country}</td>
<td>{c.postalCode}</td>
<td>{c.phone}</td>
<td>{c.email}</td>
<td>{c.age}</td>
<td>
<Button
variant="outline-primary"
onClick={editContact.bind(this, c)}
>
Edit
</Button>
</td>
<td>
<Button
variant="outline-primary"
onClick={deleteSelectedContact.bind(this, c.id)}
>
Delete
</Button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
</div>
);
}
const mapStateToProps = state => {
return {
contacts: state.contacts
};
};
const mapDispatchToProps = dispatch => ({
setCurrentContact: contact => dispatch(setCurrentContact(contact))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(HomePage);
This is the home page of our app. We have a button to open the modal and a table to display the address book entries. Also, we have functions to open and close the modal with the openModal
, closeModal
, cancelAddModal
, and cancelEditModal
functions. We have a modal for add and another for editing contacts, they open the same form. When the edit modal is open with the editContact
function, setCurrentContact
provided by the mapDispatchToProps
function via the props is run, setting the current contact being edited so that it can be retrieved for the initialValues
of our ContactForm
component by calling the connect
function like we did. We have Edit and Delete buttons to call editContact
and deleteSelectedContact
respectively.
In index.js
, replace the existing code with:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";
import { Provider } from "react-redux";
import { contactsReducer, currentContactReducer } from "./reducers";
const rootReducer = combineReducers({
form: formReducer,
contacts: contactsReducer,
currentContact: currentContactReducer
});
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();
to inject our Redux store so that it’s available throughout our app.
Then create reducers.js
and add:
import { SET_CONTACTS, LOAD } from "./actions";
function contactsReducer(state = {}, action) {
switch (action.type) {
case SET_CONTACTS:
state = JSON.parse(JSON.stringify(action.payload));
return state;
default:
return state;
}
}
function currentContactReducer(state = {}, action) {
switch (action.type) {
case LOAD:
return {
data: action.data
};
default:
return state;
}
}
export { contactsReducer, currentContactReducer };
to create the 2 reducers that we mentioned before.
Next, create RenderCountryField.js
and add:
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import { COUNTRIES } from "./exports";
export const renderCountryField = ({
input,
label,
type,
meta: { touched, error, warning }
}) => (
<Form.Row>
<Form.Group as={Col} md="12" controlId="country">
<Form.Label>{label}</Form.Label>
<Form.Control
as="select"
type={type}
placeholder={label}
{...input}
isInvalid={touched && error}
>
{COUNTRIES.map(c => (
<option key={c} value={c}>
{c}
</option>
))}
</Form.Control>
<Form.Control.Feedback type="invalid">
{touched && error}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
);
to create our country drop-down with form validation, which we pass into the component
prop of the Field
component in ContactForm.js
.
Next create RenderInputField.js
and add:
import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
export const renderInputField = ({
input,
label,
type,
meta: { touched, error, warning }
}) => (
<Form.Row>
<Form.Group as={Col} md="12">
<Form.Label>{label}</Form.Label>
<Form.Control
type={type}
placeholder={label}
{...input}
isInvalid={touched && error}
/>
<Form.Control.Feedback type="invalid">
{touched &&
((error && <span>{error}</span>) ||
(warning && <span>{warning}</span>))}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
);
to create the text input for our contact form.
Both inputs use the Form
component from React Bootstrap.
Next, create requests.js
and add:
const APIURL = 'http://localhost:3000';
const axios = require('axios');
export const getContacts = () => axios.get(`${APIURL}/contacts`);
export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);
export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);
export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);
to create the functions for making the HTTP requests to our back end to persist our contact data.
Finally, in index.html
, replace the existing code with:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React Address Book App</title>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
to change our app title and add our Bootstrap CSS.
Now we can run the app by running set PORT=3001 && react-scripts start
on Windows or PORT=3006 react-scripts start
on Linux.
To start the back end, we first install the json-server
package by running npm i json-server
. Then, go to our project folder and run:
json-server --watch db.json
In db.json
, change the text to:
{
"contacts": [
]
}
So we have the contacts
endpoints defined in the requests.js
available.