How to Build Your React UI with Reactstrap

John Au-Yeung - Jan 26 '20 - - Dev Community

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Even more articles at http://thewebdev.info/

React is a simple library for building interactive frontend web apps. It has a simple API and focuses entirely on the view layer. The core of React is the component architecture which allows developers to build modular and intuitive web apps. Bootstrap is a UI framework made by Twitter that provides default CSS and UI elements. It has been adapted for React by creating components to match the Bootstrap UI elements. Reactstrap is one of the libraries that provides Boostrap-styled React components.

In this story, we will build an address book app using Reactstrap along with Formik and Yup which integrates seamlessly to build forms. To start we use Create React App to scaffold the app. We run npx create-react-app address-book to create the app project folder with the initial files.

The app will have a home page to display the contacts and let us open a modal to add a contact. There will be a table that displays all the contacts and Edit and Delete buttons. The contacts will be stored in a central Redux store, making them easy to access. React Router will be used for routing. Contacts will be saved in the backend and accessed through an API using the JSON server package.

To install the libraries we mentioned above, we run npm i axios bootstrap formik reactstrap react-redux react-router-dom yup. Axios is the HTTP client that we use for making HTTP requests to back end. react-router-dom is the package name for the latest version of React Router for the web.

Now that we have all the libraries installed, we can start building the app. All files will be in the src folder unless mentioned otherwise. First, we work on the Redux store. We create a file called actionCreator.js in the src folder and add the following:

import { SET_CONTACTS } from './actions';
const setContacts = (contacts) => {
    return {
        type: SET_CONTACTS,
        payload: contacts
    }
};
export { setContacts };

This is the action creator for storing the contacts in the store.

We create another file called actions.js and add our constant for dispatching the action:

const SET_CONTACTS = 'SET_CONTACTS';
export { SET_CONTACTS };

In App.js , we replace what is existing with the following:

import React, { useState } from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import {
  Collapse,
  Navbar,
  NavbarToggler,
  NavbarBrand,
  Nav,
  NavItem,
  NavLink,
} from "reactstrap";
const history = createHistory();
function App() {
  const [isOpen, setIsOpen] = useState(false);
  const toggle = () => {
    setIsOpen(!isOpen);
  };
  return (
    <div className="App">
      <Router history={history}>
        <Navbar color="light" light expand="md">
          <NavbarBrand href="/">Address Book</NavbarBrand>
          <NavbarToggler onClick={toggle} />
          <Collapse isOpen={isOpen} navbar>
            <Nav className="ml-auto" navbar>
              <NavItem>
                <NavLink href="/">Home</NavLink>
              </NavItem>
            </Nav>
          </Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>
  );
}
export default App;

We use the Navbar component provided by Reactstrap for adding the top bar. It is made to be responsive with the NavToggler component and the Collapse component.

In App.css , we replace the existing code with:

.App {  
  text-align: center;  
}

This centers the text.

Next we need to create an input component that will be used by the Formik form since Reactstrap does not have built-in support form Formik’s form handlers. In the src folder, add an input.js and add the following:

import React from "react";
import { Input, FormFeedback } from "reactstrap";
const input = ({ field, form: { touched, errors }, ...props }) => {
  return (
    <div>
      <Input
        invalid={!!(touched[field.name] && errors[field.name])}
        {...field}
        {...props}
        value={field.value || ""}
      />
      {touched[field.name] && errors[field.name] && (
        <FormFeedback>{errors[field.name]}</FormFeedback>
      )}
    </div>
  );
};
export default input;

We use the Input component from Reactstrap here for accepting input and the FormFeedback component for displaying form validation errors. The touched and errors props will be passed in from the Formik Field component.

Next we need a list of countries to populate the country dropdown of the contact form. Create a file called export.js in the src folder and add:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; 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 &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; 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",
];

This file will be imported by the ContactForm component.

Now we have all the parts to create the contact form. Create a file called ContactForm.js and add the following code:

import React from "react";
import { Formik, Field } from "formik";
import * as yup from "yup";
import { COUNTRIES } from "./exports";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";
import { connect } from "react-redux";
import { setContacts } from "./actionCreators";
import {
  Button,
  Form,
  FormGroup,
  Label,
  Input,
  FormFeedback,
} from "reactstrap";
import input from "./input";
const schema = yup.object({
  firstName: yup.string().required("First name is required"),
  lastName: yup.string().required("Last name is required"),
  address: yup.string().required("Address is required"),
  city: yup.string().required("City is required"),
  region: yup.string().required("Region is required"),
  country: yup
    .string()
    .required("Country is required")
    .default("Afghanistan"),
  postalCode: yup
    .string()
    .when("country", {
      is: "United States",
      then: yup
        .string()
        .matches(/^[0-9]{5}(?:-[0-9]{4})?$/, "Invalid postal code"),
    })
    .when("country", {
      is: "Canada",
      then: yup
        .string()
        .matches(
          /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/,
          "Invalid postal code"
        ),
    })
    .required(),
  phone: yup
    .string()
    .when("country", {
      is: country => ["United States", "Canada"].includes(country),
      then: yup
        .string()
        .matches(/^[2-9]\d{2}[2-9]\d{2}\d{4}$/, "Invalid phone nunber"),
    })
    .required(),
  email: yup
    .string()
    .email("Invalid email")
    .required("Email is required"),
  age: yup
    .number()
    .required("Age is required")
    .min(0, "Minimum age is 0")
    .max(200, "Maximum age is 200"),
});
function ContactForm({
  edit,
  onSave,
  setContacts,
  contact,
  onCancelAdd,
  onCancelEdit,
}) {
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addContact(evt);
    } else {
      await editContact(evt);
    }
    const response = await getContacts();
    setContacts(response.data);
    onSave();
  };
  return (
    <div className="form">
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={contact || {}}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <FormGroup>
              <Label>First name</Label>
              <Field name="firstName" type="text" component={input} />
            </FormGroup>
            <FormGroup>
              <Label>Last name</Label>
              <Field name="lastName" type="text" component={input} />
            </FormGroup>
            <FormGroup>
              <Label>Address</Label>
              <Field name="address" type="text" component={input} />
            </FormGroup>
            <FormGroup>
              <Label>City</Label>
              <Field name="city" type="text" component={input} />
            </FormGroup>
            <FormGroup>
              <Label>Region</Label>
              <Field name="region" type="text" component={input} />
            </FormGroup>
<FormGroup>
              <Label>Country</Label>
              <Field
                name="country"
                component={({ field, form: { touched, errors }, ...props }) => {
                  return (
                    <div>
                      <Input
                        invalid={!!(touched[field.name] && errors[field.name])}
                        {...field}
                        {...props}
                        type="select"
                        value={field.value || ""}
                      >
                        {COUNTRIES.map(c => (
                          <option key={c} value={c}>
                            {c}
                          </option>
                        ))}
                      </Input>
                      {touched[field.name] && errors[field.name] && (
                        <FormFeedback>{errors[field.name]}</FormFeedback>
                      )}
                    </div>
                  );
                }}
              />
              <FormFeedback type="invalid">{errors.country}</FormFeedback>
            </FormGroup>
<FormGroup>
              <Label>Postal Code</Label>
              <Field name="postalCode" type="text" component={input} />
            </FormGroup>
<FormGroup>
              <Label>Phone</Label>
              <Field name="phone" type="text" component={input} />
            </FormGroup>
<FormGroup>
              <Label>Email</Label>
              <Field name="email" type="text" component={input} />
            </FormGroup>
<FormGroup>
              <Label>Age</Label>
              <Field name="age" type="text" component={input} />
            </FormGroup>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Save
            </Button>
            <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
              Cancel
            </Button>
          </Form>
        )}
      </Formik>
    </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)),
});
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(ContactForm);

We use the Field component from Formik, which lets us handle the change of input values and set them as they are entered. In the component prop of each Field component, we pass in the Reactstrap input that we added in input.js or for the country drop down, we pass in an Input component with type select with the countries options as the child of the select Input. We passed in the input validation schema, the schema object, created by using the Yup library so that we can use it in our Formik form. Form validation will be handled automatically by the validation schema since we are using Formik’s form fields for inputs.

The handleSubmit function takes the form data stored in the evt object, then save it if the data is valid, and then set the contacts in the Redux store. And the onSave function, which is passed from the HomePage component, is called so that we notify the HomePage component that saving contact is done, so that it will refresh the page.

Next we create the home page. Create a file called HomePage.js in the src folder and add:

import React from "react";
import { useState, useEffect } from "react";
import {
  Button,
  Modal,
  ModalHeader,
  ModalBody,
  ButtonToolbar,
  Table,
} from "reactstrap";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { connect } from "react-redux";
import { getContacts, deleteContact } from "./requests";
function HomePage() {
  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);
    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 isOpen={openAddModal}>
        <ModalHeader>Add Contact</ModalHeader>
        <ModalBody>
          <ContactForm
            edit={false}
            onSave={closeModal.bind(this)}
            onCancelAdd={cancelAddModal}
          />
        </ModalBody>
      </Modal>
<Modal isOpen={openEditModal}>
        <ModalHeader>Edit Contact</ModalHeader>
        <ModalBody>
          <ContactForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedContact}
            onCancelEdit={cancelEditModal}
          />
        </ModalBody>
      </Modal>
      <ButtonToolbar>
        <Button variant="outline-primary" onClick={openModal}>
          Add Contact
        </Button>
      </ButtonToolbar>
      <br />
      <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>
  );
}
const mapStateToProps = state => {
  return {
    contacts: state.contacts,
  };
};
export default connect(
  mapStateToProps,
  null
)(HomePage);

On this page, we have the table for listing the contacts’s data and, we add buttons add, edit, and delete contacts. To toggle the Reactstrap modal, we set the openAddModal and openEditModal flags respectively.

The table and buttons are provided by Reactstrap.

We use useEffect’s callback function to get the data during the initial load, and then set the initalized flag to true after data is loaded by calling the getData function to stop it from getting data again.

We pass the closeModal function to the onSave prop so that it will close the modal after the data in the ContactForm component is saved. We pass in the same function with the onCancelEdit prop of the ContactForm.

In index.js, we 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 { contactsReducer } from './reducers';
import { Provider } from 'react-redux'
import { createStore, combineReducers } from 'redux'
const addressBookApp = combineReducers({
    contacts: contactsReducer,
})
const store = createStore(addressBookApp)
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
serviceWorker.unregister();

We combined the reducers and create the store, then inject it into our app with the Provider component so that we can use it everywhere.

Next we add the reducer for our Redux store. Add a file called reducers.js in the src folder and add:

import { SET_CONTACTS } from './actions';
function contactsReducer(state = {}, action) {
    switch (action.type) {
        case SET_CONTACTS:
            state = JSON.parse(JSON.stringify(action.payload));
            return state;
        default:
            return state
    }
}
export { contactsReducer };

This is the reducer where we store the contacts that we dispatch by calling the prop provided by the mapDispatchToProps function in our components.

Then we make a file called 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}`);

These are the functions making our HTTP requests to the backend to save and delete contacts.

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 back end, we 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": [
  ]
}

This provides the contacts endpoints defined in requests.js available.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .