Categories
React

Add Google Maps to a React App

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. Because of this, adding third party libraries is easy. Google Maps is a popular map system that can be incorporated into a React app because developers have wrote a component for it.

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. It will have a Google map for displaying the contact’s location. When the user clicks the Map button in each row, the user will see the map in a modal.

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 the country. Bootstrap forms can be used seamlessly with Formik and Yup.

We use react-google-maps to incorporate Google Maps into our React app, located at https://www.npmjs.com/package/react-google-maps. It is popular and it is actively maintained so it will work with the latest version of React. It is also easy to use.

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 react-google-maps . 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 save 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.

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 the 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.

Next, we make the Google Map component for displaying the contact’s location. We make a file called MapComponent.js and add:

import React from "react";  
import { compose, withProps } from "recompose";  
import { GOOGLE_API_KEY } from "./requests";import {  
  withGoogleMap,  
  GoogleMap,  
  Marker,  
  withScriptjs,  
} from "react-google-maps";const MapComponent = compose(  
  withProps({  
    googleMapURL: `[https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&v=3.exp&libraries=geometry,drawing,places`](https://maps.googleapis.com/maps/api/js?key=${GOOGLE_API_KEY}&v=3.exp&libraries=geometry,drawing,places`),  
    loadingElement: <div style={{ height: `100%` }} />,  
    containerElement: <div style={{ height: `400px` }} />,  
    mapElement: <div style={{ height: `100%` }} />,  
  }),  
  withScriptjs,  
  withGoogleMap  
)(({ lat, lng }) => (  
  <GoogleMap defaultZoom={8} defaultCenter={{ lat, lng }}>  
    <Marker position={{ lat, lng }} />  
  </GoogleMap>  
));

export default MapComponent;

The fields in the argument of the withProps function call are props required for the GoogleMap component, withScriptjs loads the Google Map JavaScript files from Google. lat and lng are props that we pass into the component to display the location by latitude and longitude respectively.

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 MapComponent from "./MapComponent";  
import { connect } from "react-redux";  
import { getContacts, deleteContact, getLatLng } from "./requests";function HomePage() {  
  const [openAddModal, setOpenAddModal] = useState(false);  
  const [openEditModal, setOpenEditModal] = useState(false);  
  const [openMapModal, setOpenMapModal] = useState(false);  
  const [initialized, setInitialized] = useState(false);  
  const [loc, setLoc] = useState({  
    lat: 0,  
    lng: 0,  
  });  
  const [selectedContact, setSelectedContact] = useState({});  
  const [contacts, setContacts] = useState([]); 
  const openModal = () => {  
    setOpenAddModal(true);  
  }; 

  const closeModal = () => {  
    setOpenAddModal(false);  
    setOpenEditModal(false);  
    setOpenMapModal(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();  
  }; 

  const openMap = async contact => {  
    try {  
      const address = `${contact.addressLineOne}, ${contact.addressLineTwo}, ${contact.city}, ${contact.country}`;  
      const response = await getLatLng(address);  
      const loc = response.data.results[0].geometry.location;  
      setLoc(loc);  
      setOpenMapModal(true);  
    } catch (ex) {  
      console.log(ex);  
    }  
  }; 

  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> <Modal show={openMapModal} onHide={closeModal}>  
        <Modal.Header closeButton>  
          <Modal.Title>Map</Modal.Title>  
        </Modal.Header>  
        <Modal.Body>  
          <MapComponent  
            lat={loc.lat}  
            lng={loc.lng}  
          />  
        </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>Map</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={openMap.bind(this, c)}  
                >  
                  Map  
                </Button>  
              </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 to display the contacts and buttons to add, edit, and delete contact. It gets data once on the first load with the getData function call in the useEffect‘s callback function. useEffect‘s callback is called on every renders so we want to set a initialized flag and check that it loads only if it’s true.

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](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 MAPURL = "https://maps.googleapis.com/maps/api/geocode/json?address=";  
const axios = require("axios");  
export const GOOGLE_API_KEY = "your API key";

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}`);

export const getLatLng = address => {  
  return axios.get(  
    `${MAPURL}${encodeURIComponent(address)}&key=${GOOGLE_API_KEY}`  
  );  
};

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/](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.

Categories
React Projects

Create an RSS Reader with React and JavaScript

React is an easy to use JavaScript framework that lets us create front end apps.

In this article, we’ll look at how to create an RSS reader with React and JavaScript.

Create the Project

We can create the React project with Create React App.

To install it, we run:

npx create-react-app rss-reader

with NPM to create our React project.

Create the RSS Reader

To create the RSS reader, we write:

import React, { useState } from "react";

export default function App() {
  const [rssUrl, setRssUrl] = useState("");
  const [items, setItems] = useState([]);

  const getRss = async (e) => {
    e.preventDefault();
    const urlRegex = /(http|ftp|https):\/\/[\w-]+(\.[\w-]+)+([\w.,@?^=%&amp;:\/~+#-]*[\w@?^=%&amp;\/~+#-])?/;
    if (!urlRegex.test(rssUrl)) {
      return;
    }
    const res = await fetch(`https://api.allorigins.win/get?url=${rssUrl}`);
    const { contents } = await res.json();
    const feed = new window.DOMParser().parseFromString(contents, "text/xml");
    const items = feed.querySelectorAll("item");
    const feedItems = [...items].map((el) => ({
      link: el.querySelector("link").innerHTML,
      title: el.querySelector("title").innerHTML,
      author: el.querySelector("author").innerHTML
    }));
    setItems(feedItems);
  };

  return (
    <div className="App">
      <form onSubmit={getRss}>
        <div>
          <label> rss url</label>
          <br />
          <input onChange={(e) => setRssUrl(e.target.value)} value={rssUrl} />
        </div>
        <input type="submit" />
      </form>
      {items.map((item) => {
        return (
          <div>
            <h1>{item.title}</h1>
            <p>{item.author}</p>
            <a href={item.link}>{item.link}</a>
          </div>
        );
      })}
    </div>
  );
}

We have the rssUrl state that stores the RSS feed URL we enter.

And the items state store the feed items which we’ll get.

Then we define the getRSS function to get the RSS feed items when we click submit.

Next, we call e.preventDefault() to let us do client side submission.

Then we check if the rssUrl value is a valid URL string.

If it is, then we make a GET request with fetch to the RSS feed via a CORS proxy.

We use the allOrigins API to let us make cross-domain requests to the given feed.

We need this since cross-domain communication from the browser isn’t allows with most feeds.

We get the contents property from the object.

Then we parse the XML string with the DOMParser instance’s parseFromString method.

The first argument is the XML string.

And the 2nd argument is the content-type string.

feed is the XML DOM object with the whole XML tree.

Then we use querySelectorAll to get all the item nodes.

And then we use the spread operator to spread the items Node list to an array.

Then we can call map on it to select nodes inside the item nodes with querySelector and return an object with the innerHTML values.

We call setItems to set the items state.

Below that, we have the form with the onSubmit prop set to getRSS to let us run the function when we click on the input with type submit .

Inside the form, we have an input with the onChange prop set to a function that calls setRssUrl to set the inputted value as the value of the rssUrl state.

e.target.value has the inputted value.

value is set to rssUrl so that we can see the inputted value.

Below the form, we render the items into divs with the map method.

Conclusion

We can create an RSS reader easily with React and JavaScript.

Categories
React

How to Pass React Props Safely with Prop Type Validation

React is a simple library for creating 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. One frequently used feature is the passing of props between parent and child components. You can pass in any kind of object from parent to child. In the parent, you add attributes to your child component and pass in the objects from the parent to pass in props from parent to child.

Prop data types are not checked by default. This means that mistakes can easily happen. Therefore, the React library provides data type validation for props. You can validate that it is a string, number, object, function, etc. The full details are located at https://reactjs.org/docs/typechecking-with-proptypes.html.

For form validation, then you need to use a third party library. Formik and Yup work great together to allow us to take care of most form validation needs. Formik let’s 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 the 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 only has 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;
}

This centers 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 lets 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.

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 .

Notice that we validate the prop types here. We make sureonSave, onCancelAdd and onCancelEdit are functions so that we can call it. And we make sure contact is an object so that we can edit it in the form, and edit should be boolean since we will pass in a boolean flag to it.

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 .

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.

The type checking will come in handy here. If we pass in something that fails the type check, warning will be triggered, allowing us to correct it quickly.

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](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'](http://localhost:3000%27);
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/](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](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.

Categories
React Projects

Create a Stopwatch App with React and JavaScript

React is an easy to use JavaScript framework that lets us create front end apps.

In this article, we’ll look at how to create a stopwatch app with React and JavaScript.

Create the Project

We can create the React project with Create React App.

To install it, we run:

npx create-react-app stopwatch

with NPM to create our React project.

Also, we have to install the moment library to make calculating and formatting the duration easier.

To install it, we run:

npm i moment

Create the Stopwatch App

To create the stopwatch, we write:

import React, { useState } from "react";
import moment from "moment";

export default function App() {
  const [startDate, setStartDate] = useState(new Date());
  const [diff, setDiff] = useState("00:00:00");
  const [timer, setTimer] = useState();

  return (
    <div className="App">
      <button
        onClick={() => {
          setStartDate(new Date());
          const timer = setInterval(() => {
            let start = moment(startDate);
            let end = moment(new Date());
            let diff = end.diff(start);
            let f = moment.utc(diff).format("HH:mm:ss.SSS");
            setDiff(f);
          }, 1000);
          setTimer(timer);
        }}
      >
        start
      </button>
      <button onClick={() => clearInterval(timer)}>stop</button>
      <p>{diff}</p>
    </div>
  );
}

We have the startDate state to store the start date time.

diff stores the elapsed time as a string.

timer has the timer object.

Then we add a button with a function that calls setStartDate to set the start date.

Then we create a timer that runs every second with setInterval .

In the setInterval callback, we create moment objects from the startDate and the current date-time.

We call moment to create those objects.

Then we get the elapsed time between them with the diff method.

And then we get the elapsed time formatted into a string with the moment.utc method and the format method.

Also, we call setTimer to set the timer state to our setIntervaltimer.

Next, we have the stop button to call clearInterval to stop the timer.

And the p element displays the formatted elapsed time string.

Conclusion

We can create a stopwatch easily with React and JavaScript.

Categories
React

How to Focus Something on Next Render with React Hooks?

We may want to focus something on the next render within a component created with React hooks.

In this article, we’ll look at how to focus on an element on the next render within a component created with React hooks.

Focus Something on Next Render with React Hooks

To focus on an element on the next render, we can write something like the following:

import { useEffect, useRef, useState } from "react";

export default function App() {
  const [isEditing, setEditing] = useState(false);
  const toggleEditing = () => {
    setEditing(!isEditing);
  };

  const inputRef = useRef(null);

  useEffect(() => {
    if (isEditing) {
      inputRef.current.focus();
    }
  }, [isEditing]);

  return (
    <div>
      {isEditing && <input ref={inputRef} />}
      <button onClick={toggleEditing}>edit</button>
    </div>
  );
}

We create the isEditing state with the useState hook.

It’s set to false initially.

Next, we add the toggleEditing function to call setEditing function to the negated value of the isEditing state.

Then we create the inputRef ref that lets us assign to the input element below.

Next, we have the useEffect hook that runs when the isEditing state is changed.

We check the isEditing value, and if it’s true , we call inputRef.current.focus() to focus on the input.

The 2nd argument is set to [isEditing] so that isEditing ‘s value is watched.

Then we render the input if isEditing is true .

And in the input, we assigned a ref to it.

And we have a button that runs the toggleEditing function when we click on edit.

Conclusion

We can focus on an element on next render easily by watching a state’s value with useEffect .

Then we can check the value of the state and call focus on the element object, which is the ref which we assigned to the element.