How to Build Your React UI with Reactstrap
Tags: javascript, webdev, react, tutorial
Published: 2020-01-26 23:54:36

**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](https://github.com/typicode/json-server).

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:

```javascript
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:

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

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

```jsx
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:

```css
.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:

```jsx
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:

```javascript
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:

```jsx
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:

```jsx
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:

```jsx
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:

```javascript
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:

```javascript
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:

```bash
json-server --watch db.json
```

In `db.json`, change the text to:

```
{
  "contacts": [
  ]
}
```

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