Categories
React

React App Routing with Wouter — Route Changes, Switches, and Links

Wouter is a library that lets us loading React components according to URLs.

In this article, we’ll look at how to add routing to a React app with Wouter.

Watch Router Changes with useRouter

We can use the useRouter hook to watch for changes to the router object.

For instance, we write:

import React, { useEffect } from "react";
import { Link, Route, Router, useRouter } from "wouter";

const InboxPage = () => {
  return (
    <div>
      <p>inbox</p>
    </div>
  );
};

export default function App() {
  const router = useRouter();

  useEffect(() => {
    console.log(router);
  }, [router]);

  return (
    <div>
      <Link href="/inbox">
        <a>Inbox</a>
      </Link>
      <Router>
        <Route path="/about">
          <p>About Us</p>
        </Route>
        <Route path="/users/:name">
          {(params) => <div>Hello, {params.name}!</div>}
        </Route>
        <Route path="/inbox" component={InboxPage} />
      </Router>
    </div>
  );
}

We use the useEffect hook to watch for router changes.

router has the hook property, which is the useLocation hook by default.

Also, we can add our own properties to the router object to pass data between routes

Route Component

We can use the Route component to define routes.

For instance, we write:

import React, { useEffect } from "react";
import { Link, Route, Router, useRouter } from "wouter";

const InboxPage = () => {
  return (
    <div>
      <p>inbox</p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <Link href="/inbox">
        <a>Inbox</a>
      </Link>
      <Router>
        <Route path="/about">
          <p>About Us</p>
        </Route>
        <Route path="/users/:name">
          {(params) => <div>Hello, {params.name}!</div>}
        </Route>
        <Route path="/inbox" component={InboxPage} />
      </Router>
    </div>
  );
}

We add the Route component and set the path prop to define the routes.

:name is a URL parameter placeholder.

And we can get its value from the params parameter.

Link Component

The Link component lets us add links to let us navigate to different pages.

For instance, we write:

import React, { useEffect } from "react";
import { Link, Route, Router, useRouter } from "wouter";

const InboxPage = () => {
  return (
    <div>
      <p>inbox</p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <Link href="/about">
        <a>About</a>
      </Link>
      <Router>
        <Route path="/about">
          <p>About Us</p>
        </Route>
        <Route path="/users/:name">
          {(params) => <div>Hello, {params.name}!</div>}
        </Route>
        <Route path="/inbox" component={InboxPage} />
      </Router>
    </div>
  );
}

to add a link to go to the About page when we click it.

Switch Component

The Switch component lets us add exclusive routing into our React app.

It makes sure that only one route is rendered at a time.

For instance, we write:

import React from "react";
import { Link, Route, Switch } from "wouter";

const AllOrders = () => {
  return (
    <div>
      <p>all orders</p>
    </div>
  );
};

const Orders = ({ params }) => {
  return (
    <div>
      <p>orders {params.status}</p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <Link href="/orders/all">
        <a>All</a>
      </Link>
      <Switch>
        <Route path="/orders/all" component={AllOrders} />
        <Route path="/orders/:status" component={Orders} />
      </Switch>
    </div>
  );
}

We put the Route components in a Switch so that we only AllOrders or Orders but never both together.

In Orders , we get the URL parameters from the params prop.

Conclusion

We can add exclusive routes and watch for router changes in our React app with Wouter.

Categories
React

Add Routing to a React App with Wouter

Wouter is a library that lets us loading React components according to URLs.

In this article, we’ll look at how to add routing to a React app with Wouter.

Installation

To install it, we run:

npm i wouter

Getting Started

We can add routes to our React app by using the Route component.

For instance, we can write:

import React from "react";
import { Link, Route } from "wouter";

const InboxPage = () => <p>inbox</p>;

export default function App() {
  return (
    <div>
      <Link href="/users/1">
        <a className="link">Profile</a>
      </Link>

      <Route path="/about">About Us</Route>
      <Route path="/users/:name">
        {(params) => <div>Hello, {params.name}!</div>}
      </Route>
      <Route path="/inbox" component={InboxPage} />
    </div>
  );
}

We add the Router component to let us add map routes to React components.

:name is a URL parameter.

We can get its value with params.name .

Link lets us create links to load routes.

We can also add the component prop to add components as we did with InboxPage .

useRoute Hook

We can use the useRouter hook to extract URL parameters from a route.

For example, we write:

import React from "react";
import { Transition } from "react-transition-group";
import { useRoute } from "wouter";

export default function App() {
  const [match, params] = useRoute("/users/:id");

  return (
    <div>
      <Transition in={match}>
        <p>Hi, this is: {params.id}</p>
      </Transition>
    </div>
  );
}

We parse the route URL with tyhe useRoute hook by passing it into the hook.

Then we get the params object to get the URL parameters.

We also have the Transition component to add transitions when we load routes.

useLocation Hook

We can use the useLocation hook to return the location object to get go to a different route.

For example, we write:

import React from "react";
import { Link, Route, useLocation } from "wouter";

const InboxPage = () => {
  const [location, setLocation] = useLocation();

  return (
    <div>
      <p>{`The current page is: ${location}`}</p>
      <p>
        <a onClick={() => setLocation("/about")}>Click to update</a>
      </p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <Link href="/inbox">
        <a>Inbox</a>
      </Link>
      <Route path="/about">
        <p>About Us</p>
      </Route>
      <Route path="/users/:name">
        {(params) => <div>Hello, {params.name}!</div>}
      </Route>
      <Route path="/inbox" component={InboxPage} />
    </div>
  );
}

to add a link to the InboxPage component.

The link uses the useLocation hook’s setLocation function to go to the about route.

So when we click the Click to update link, we see About us.

Customizing the Location Hook

We can customize the useLocation hook to do what we want.

For instance, we can extract URL segments from the hash instead of from URL parameters.

To do this, we write:

import React, { useCallback, useEffect, useState } from "react";
import { Link, Route, Router, useLocation } from "wouter";

const currentLocation = () => {
  return window.location.hash.replace(/^#/, "") || "/";
};

const useHashLocation = () => {
  const [loc, setLoc] = useState(currentLocation());

  useEffect(() => {
    const handler = () => setLoc(currentLocation());
    window.addEventListener("hashchange", handler);
    return () => window.removeEventListener("hashchange", handler);
  }, []);
  const navigate = useCallback((to) => (window.location.hash = to), []);
  return [loc, navigate];
};

const InboxPage = () => {
  const [location] = useLocation();

  return (
    <div>
      <p>{`The current page is: ${location}`}</p>
    </div>
  );
};

export default function App() {
  return (
    <div>
      <Link href="/inbox">
        <a>Inbox</a>
      </Link>
      <Router hook={useHashLocation}>
        <Route path="/about">
          <p>About Us</p>
        </Route>
        <Route path="/users/:name">
          {(params) => <div>Hello, {params.name}!</div>}
        </Route>
        <Route path="/inbox" component={InboxPage} />
      </Router>
    </div>
  );
}

We have the currentLocation function that gets the part of the URL after thw hash.

Then we pass that into the useState hook to set the initial URL hash value.

And we listen for hashchange events to get the changes to the hash part of the URL.

If there are any changes, we return the loc location.

navigate navigates to the hash part of the URL.

Then we pass the hook into the hook prop to use it.

So now when we go to /#/users/1 , we see Hello, 1! displayed.

When we go to /#/inbox , we see The current page is: /inbox

And going to /#/about renders About Us .

Conclusion

Wouter is a lightweight router for React apps.

Categories
React

More Ways to Render Large Lists with React

We often have to render large lists in React apps.

In this article, we’ll look at ways to render large lists in React apps.

Virtual Scrolling

The react-virtualized library lets us add a virtualized list into our React app.

It only loads data that is shown on the screen.

To use it, we install it by running:

npm i react-virtualized

Then we can use it bu writing:

import React from "react";
import { List } from "react-virtualized";

export default function App() {
  const data = new Array(1000).fill().map((value, id) => ({
    id: id,
    title: id,
    body: id
  }));

  const renderRow = ({ index, key, style }) => (
    <div>
      <div key={key} style={style} className="post">
        <h3>{data[index].title}</h3>
        <p>item {data[index].body}</p>
      </div>
    </div>
  );
  return (
    <List
      width={window.innerWidth * 0.95}
      height={300}
      rowRenderer={renderRow}
      rowCount={data.length}
      rowHeight={120}
    />
  );
}

We just create the renderRiw function to render the items by their index and style .

The style is computed by react-virtualized to fit the data in the list.

We get the row from data[index] .

rowCount is set to the total row count.

height has the height of the list.

rowHeight has the row’s height.

react-window

The react-window is another virtualized scrolling component.

To install it, we run:

npm i react-window

Then we can add the list by writing:

import React from "react";
import { FixedSizeList as List } from "react-window";

export default function App() {
  const data = new Array(1000).fill().map((value, id) => ({
    id: id,
    title: id,
    body: id
  }));

  const Row = ({ index, key, style }) => (
    <div>
      <div key={key} style={style} className="post">
        <h3>{data[index].title}</h3>
        <p>item {data[index].body}</p>
      </div>
    </div>
  );
  return (
    <List
      width={window.innerWidth * 0.95}
      height={300}
      itemCount={data.length}
      itemSize={120}
    >
      {Row}
    </List>
  );
}

We have the Row component that has the row data.

It’s the same as the previous example’s renderRow function.

We have the List component with the width and height so it can compute the row sizes.

itemSize has the row’s height.

itemCount has the number of items to render.

react-window is faster and smaller than react-virtualized .

Conclusion

We can render large lists with virtual scrolling.

Categories
React

Ways to Render Large Lists with React

We often have to render large lists in React apps.

In this article, we’ll look at ways to render large lists in React apps.

Pagination

We can use the react-paginate library to add pagination to our React lists.

To install it, we run:

npm i react-paginate

Then we can use it by writing:

import React, { useEffect, useState } from "react";
import ReactPaginate from "react-paginate";

export default function App() {
  const [pagination, setPagination] = useState({
    data: new Array(1000).fill().map((value, index) => ({
      id: index,
      title: index,
      body: index
    })),
    offset: 0,
    numberPerPage: 10,
    pageCount: 0,
    currentData: []
  });
  useEffect(() => {
    setPagination((prevState) => ({
      ...prevState,
      pageCount: prevState.data.length / prevState.numberPerPage,
      currentData: prevState.data.slice(
        pagination.offset,
        pagination.offset + pagination.numberPerPage
      )
    }));
  }, [pagination.numberPerPage, pagination.offset]);
  const handlePageClick = (event) => {
    const selected = event.selected;
    const offset = selected * pagination.numberPerPage;
    setPagination({ ...pagination, offset });
  };
  return (
    <div>
      {pagination.currentData &&
        pagination.currentData.map((item, index) => (
          <div key={item.id} className="post">
            <h3>{item.title}</h3>
            <p>item {item.body}</p>
          </div>
        ))}
      <ReactPaginate
        previousLabel="previous"
        nextLabel="next"
        breakLabel="..."
        pageCount={pagination.pageCount}
        marginPagesDisplayed={2}
        pageRangeDisplayed={5}
        onPageChange={handlePageClick}
        containerClassName={"pagination"}
        activeClassName={"active"}
      />
    </div>
  );
}

The ReactPaginate component lets us add pagination controls into our React app.

We have the handlePageClick method to get the data from the array and slice it to get the items we want given the page number.

currentData has the data for the current page.

We get the page number from the event.selected property.

With pagination, we load a small amount of data per page.

Infinite Scroll

Another way to load large amounts of data is to use infinite scrolling.

When we scroll down, we tet more data attached to the list.

To add infinite scrolling, we can use the react-infinite-scroll-component library.

We can install it by running:

npm i react-infinite-scroll-component

Then we can use it by writing:

import React, { useState } from "react";
import InfiniteScroll from "react-infinite-scroll-component";

export default function App() {
  const data = new Array(1000).fill().map((value, id) => ({
    id: id,
    title: id,
    body: id
  }));

  const [count, setCount] = useState({
    prev: 0,
    next: 10
  });
  const [hasMore, setHasMore] = useState(true);
  const [current, setCurrent] = useState(data.slice(count.prev, count.next));

  const getMoreData = () => {
    if (current.length === data.length) {
      setHasMore(false);
      return;
    }
    setTimeout(() => {
      setCurrent(current.concat(data.slice(count.prev + 10, count.next + 10)));
    }, 2000);
    setCount((prevState) => ({
      prev: prevState.prev + 10,
      next: prevState.next + 10
    }));
  };

  return (
    <InfiniteScroll
      dataLength={current.length}
      next={getMoreData}
      hasMore={hasMore}
      loader={<h4>Loading...</h4>}
    >
      <div>
        {current &&
          current.map((item, index) => (
            <div key={index} className="post">
              <h3>{item.title}</h3>
              <p>item {item.body}</p>
            </div>
          ))}
      </div>
    </InfiniteScroll>
  );
}

We add the InfiniteScroll component to add the infinite scrolling container.

dataLength has the number of items to load when we scroll down.

We get more data from the array with the getMoreData method.

And we load the new data and add it to the current array in the setTimeout callback.

Then update the count.prev and count.next properties to update the indexes of the items we want to get.

Conclusion

We can add pagination and infinite scrolling into our React app to load lots of data in an efficient manner.

Categories
React

How to Use Async and Await in React Apps

React is a simple library for create interactive front end web apps. Its feature set is basic. It provides you with a component based architecture for building web apps. Each component does a small thing in an app, and they can be nested in each other or put side by side. It supports all the latest JavaScript, including one very handy one, async and await . async and await is the new syntax for chaining promises, so instead of call then callbacks on each promise and returning a new promise in them, we use async and await like so:

const asyncFunction = async () => {
  const promise1Result = await promsise1;
  console.log(promise1Result);
  const promise2Result = await promsise2;
  console.log(promise2Result);
}

The code above is the same as:

const asyncFunction = () => {
  promise1
  .then((promise1Result) => {
    console.log(promise1Result);
    return promise2
  })
  .then((promise2Result) => {
    console.log(promise2Result);
  })
}

For form validation, then you need to use a third party library. Formik and Yup , located at https://github.com/jaredpalmer/formik and https://github.com/jquense/yup work great together to allow us to take care of most form validation needs. Formik let us build the forms and display the errors, and handle form value changes, which is another thing we have to do all my hand otherwise. Yup let us write a schema for validating our form fields. It can check almost anything, with common validation code like email and required fields available as built in functions. It can also check for fields that depend on other fields, like the postal code format depending on country. Bootstrap forms can be used seamlessly with Formik and Yup.

In this story, we will build an address book app, which uses those libraries, plus React Bootstrap, which has great integration with those libraries above to create forms. To start we need to run 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 on each row to edit or delete each contact. The contacts will store in a central Redux store to store the contacts in a central place, making them easy to access. React Router will be used for routing. Contacts will be saved in the back end spawned using the JSON server package, located at https://github.com/typicode/json-server.

Once that is done, we have to install some libraries. To install the libraries we mentioned above, we run npm i axios bootstrap formik react-bootstrap 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.

Now that we have all the libraries installed, we can start building the app. All files will be in the src folder except 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 creating the action for storing the contacts in the store.

We create another file called actions.js and add:

const SET_CONTACTS = 'SET_CONTACTS';

export { SET_CONTACTS };

This just have the type constant for dispatching the action.

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

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;

This is where we add the navigation bar and show our routes routed by the React Router. In App.css , we replace the existing code with:

.App {
  text-align: center;
}

to center some text.

Next we build our contact form. This is the most logic heavy part of our app. We create a file called ContactForm.js and add:

import React from 'react';
import { Formik } from 'formik';
import Form from 'react-bootstrap/Form';
import Col from 'react-bootstrap/Col';
import InputGroup from 'react-bootstrap/InputGroup';
import Button from 'react-bootstrap/Button';
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';

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}>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="firstName">
                  <Form.Label>First name</Form.Label>
                  <Form.Control
                    type="text"
                    name="firstName"
                    placeholder="First Name"
                    value={values.firstName || ''}
                    onChange={handleChange}
                    isInvalid={touched.firstName && errors.firstName}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.firstName}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="lastName">
                  <Form.Label>Last name</Form.Label>
                  <Form.Control
                    type="text"
                    name="lastName"
                    placeholder="Last Name"
                    value={values.lastName || ''}
                    onChange={handleChange}
                    isInvalid={touched.firstName && errors.lastName}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.lastName}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="address">
                  <Form.Label>Address</Form.Label>
                  <InputGroup>
                    <Form.Control
                      type="text"
                      placeholder="Address"
                      aria-describedby="inputGroupPrepend"
                      name="address"
                      value={values.address || ''}
                      onChange={handleChange}
                      isInvalid={touched.address && errors.address}
                    />
                    <Form.Control.Feedback type="invalid">
                      {errors.address}
                    </Form.Control.Feedback>
                  </InputGroup>
                </Form.Group>
              </Form.Row>
              <Form.Row>
                <Form.Group as={Col} md="12" controlId="city">
                  <Form.Label>City</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="City"
                    name="city"
                    value={values.city || ''}
                    onChange={handleChange}
                    isInvalid={touched.city && errors.city}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.city}
                  </Form.Control.Feedback>
                </Form.Group>
                <Form.Group as={Col} md="12" controlId="region">
                  <Form.Label>Region</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Region"
                    name="region"
                    value={values.region || ''}
                    onChange={handleChange}
                    isInvalid={touched.region && errors.region}
                  />
                  <Form.Control.Feedback type="invalid">
                    {errors.region}
                  </Form.Control.Feedback>
                </Form.Group>

                <Form.Group as={Col} md="12" controlId="country">
                  <Form.Label>Country</Form.Label>
                  <Form.Control
                    as="select"
                    placeholder="Country"
                    name="country"
                    onChange={handleChange}
                    value={values.country || ''}
                    isInvalid={touched.region && errors.country}>
                    {COUNTRIES.map(c => <option key={c} value={c}>{c}</option>)}
                  </Form.Control>
                  <Form.Control.Feedback type="invalid">
                    {errors.country}
                  </Form.Control.Feedback>
                </Form.Group>

                <Form.Group as={Col} md="12" controlId="postalCode">
                  <Form.Label>Postal Code</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Postal Code"
                    name="postalCode"
                    value={values.postalCode || ''}
                    onChange={handleChange}
                    isInvalid={touched.postalCode && errors.postalCode}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.postalCode}
                  </Form.Control.Feedback>
                </Form.Group>

                <Form.Group as={Col} md="12" controlId="phone">
                  <Form.Label>Phone</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Phone"
                    name="phone"
                    value={values.phone || ''}
                    onChange={handleChange}
                    isInvalid={touched.phone && errors.phone}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.phone}
                  </Form.Control.Feedback>
                </Form.Group>

                <Form.Group as={Col} md="12" controlId="email">
                  <Form.Label>Email</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Email"
                    name="email"
                    value={values.email || ''}
                    onChange={handleChange}
                    isInvalid={touched.email && errors.email}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.email}
                  </Form.Control.Feedback>
                </Form.Group>

                <Form.Group as={Col} md="12" controlId="age">
                  <Form.Label>Age</Form.Label>
                  <Form.Control
                    type="text"
                    placeholder="Age"
                    name="age"
                    value={values.age || ''}
                    onChange={handleChange}
                    isInvalid={touched.age && errors.age}
                  />

                  <Form.Control.Feedback type="invalid">
                    {errors.age}
                  </Form.Control.Feedback>
                </Form.Group>
              </Form.Row>
              <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 Formik to facilitate building our contact form here, with our Boostrap Form component nested in the Formik component so that we can use Formik’s handleChange , handleSubmit , values , touched and errors parameters. handleChange is a function that let us update the form field data from the inputs without writing the code ourselves. handleSubmit is the function that we passed into the onSubmit handler of the Formik component. The parameter in the function is the data we entered, with the field name as the key, as defined by the name attribute of each field and the value of each field as the value of those keys. Notice that in each value prop, we have ||'' so we do not get undefined values and prevent uncontrolled form warnings from getting triggered.

To display form validation messages, we have to pass in the isInvalid prop to each Form.Control component. The schema object is what Formik will check against for form validation. The argument in the required function is the validation error message. The second argument of the matches , min and max functions are also validation messages.

The parameter of the ContactForm function are props, which we will pass in from the HomePage component that we will build later. The handleSubmit function checks if the data is valid, then if it is then it will proceed to saving according to whether it is adding or editing a contact. Then when saving is successful we set the contacts in the store and call onSave prop, which is a function to close the modal the form is in. The modal will be defined in the home page.

Notice that in the handleSubmit function, we used async and await . It is much shorter than using then callbacks, which would be the alternative. We call a promise for validating the fields, then another one for saving data to back end, and another one to get the latest data.

mapStateToProps is a function provided by React Redux so that we can map the state directly to the props of our component as the function name suggests. mapDispatchToProps allows us to call function in the props of the component called setContacts to dispatch the action as we defined in actionCreators.js

Next we create a file called exports.js , and put:

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"];

These are countries for the countries field in the form.

In HomePage.js , we put:

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';

function HomePage() {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedId, setSelectedId] = useState(0);
  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 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 />
      <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);

It has the table for display the contacts and buttons to add, edit, and delete contact. It gets data once on first load with the getData function call in the useEffect ‘s callback function. useEffect ‘s callback is called on every render so we want to set a initialized flag and check that it loads only if it’s true .

useEffect does not support passing in an async function as the callback, so we have to call it inside the synchronous callback function instead.

Note that we pass in all the props in this component to the ContactForm component. To pass an argument a onClick handler function, we have to call bind on the function and pass in the argument for the function as a second argument to bind . For example, in this file, we have editContact.bind(this, c) , where c is the contact object. The editContact function is defined as follows:

const editContact = (contact) => {
  setSelectedContact(contact);
  setOpenEditModal(true);
}

c is the contact parameter we pass in.

Next we create a file called HomePage.css and put:

.home-page {
  padding: 20px;
}

to add some padding.

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 to our app with the Provider component so that we can use it everywhere in the app.

Then we make a file called reducers.js , 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 functions are making our HTTP requests to the back end to save and delete contacts.

Finally, in public/index.html , we 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" />
  <link rel="manifest" crossorigin="use-credentials" href="%PUBLIC_URL%/manifest.json" />

<!--
      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/
    -->
  <!--
      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 the title and add the Bootstrap stylesheet.

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 first install the json-server package by running npm i json-server . Them go to our project folder and run:

json-server --watch db.json

In db.json , change the text to:

{
  "contacts": [
  ]
}

so that we have the contacts endpoints defined in requests.js available.