How to Add Form Validation to React Dynamic Forms

John Au-Yeung
Dec 1 · 16 min read
Photo by Martin Schmidli on Unsplash

Dynamic forms are forms that are derived from some dynamic data. They are not hardcoded into the code. Usually, they are displayed by looping through some data and render forms from the data.

With Yup it is easy to loop through fields of an object and then add them to your validation schema, allowing you to validate form that does not have fixed fields.

An example Yup schema can be built like this:

let schemaObj = {
to: yup.string().required("This field is required"),
from: yup.string().required("This field is required"),
subject: yup.string().required("This field is required")
};
fields.forEach(f => {
schemaObj[f] = yup.string().required("This field is required");
});
let newSchema = yup.object(schemaObj);

We loop through fields to get the placeholders and add them to the schema object. That is all you need to add dynamic fields for validation with Yup.

In this article, we will build an app that lets users enter email templates. Then they can use the templates to send emails to different email addresses with the SendGrid API. Our app will consist of a back end and a front end. The front end will be built with React, and the back end will be built with Express.

SendGrid is a great service made by Twilio for sending emails. Rather than setting up your own email server for sending an email with your apps, we use SendGrid to do the hard work for us. It also decreases the chance of email ending up in spam since it is a known trustworthy service.

It also has very easy to use libraries for various platforms for sending emails. Node.js is one of the platforms that are supported.

To send emails with SendGrid, install the SendGrid SDK package by running npm i @sendgrid/mail . Then in your code, add const sgMail = require(‘@sendgrid/mail’); to import the installed package.

Then in your code, you send an email by:

sgMail.setApiKey(process.env.SENDGRID_API_KEY);
const msg = {
to: email,
from: 'email@example.com',
subject: 'Example Email',
text: `
Dear user, Here is your email.
`,
html: `
<p>Dear user,</p> <p>Here is your email.</p>
`,
};
sgMail.send(msg);

where process.env.SENDGRID_API_KEY is the SendGrid’s API, which should be stored as an environment variable since it is a secret.

Back End

To start, we will make a project folder and within it, add a backend folder inside the project folder. We will use the Express Generator to generate the code for our project. To do this, run npx express-generator inside the backend folder. Then run npm i to install the packages listed in package.json .

Next, we install our own packages. We will use Sequelize as our ORM, Babel for using the latest JavaScript features, Dotenv for storing environment variables, SendGrid Nodejs for sending emails, CORS for enabling cross-domain requests with front end and SQLite3 for the database.

To install them run npm i @babel/cli @babel/core @babel/node @babel/preset-env @sendgrid/mail cors sendgrid-nodejs sequelize sqlite3 .

With those installed, we can start building the back end. First, we add .babelrc to enable Babel in our app to run the app with the latest JavaScript interpreter. To do this, add .babelrc to the backend folder and add:

{
"presets": [
"@babel/preset-env"
]
}

Then in package.json , replace the existing code with the following in the scripts section:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

This lets us run with the latest Babel Node runtime instead of the regular Node runtime so we can use the latest JavaScript features like import

Next, run Sequelize CLI to create the database boilerplate code and migration for creating the database. Run npx sequelize-cli init in the backend folder and you will get config.json . In config.json , change the existing code to:

{
"development": {
"dialect": "sqlite",
"storage": "development.db"
},
"test": {
"dialect": "sqlite",
"storage": "test.db"
},
"production": {
"dialect": "sqlite",
"storage": "production.db"
}
}

Then we create our data migration. Run:

npx sequelize-cli model:create --name EmailTemplate --attributes name:string,type:string,template:text,subject,previewText:string

to create the EmailTemplates table in an SQLite database. The above command should also create the corresponding model for this table.

Next run npx sequelize-cli db:migrate to create the database.

Now we can move on to creating our routes. Create a file called email.js in the routes folder and add:

var express = require("express");
const models = require("../models");
const sgMail = require("@sendgrid/mail");
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
var router = express.Router();
router.get("/templates", async (req, res, next) => {
const templates = await models.EmailTemplate.findAll();
res.json(templates);
});
router.get("/template/:id", async (req, res, next) => {
const id = req.params.id;
const templates = await models.EmailTemplate.findAll({ where: { id } });
res.json(templates[0]);
});
router.post("/template", async (req, res, next) => {
try {
const template = await models.EmailTemplate.create(req.body);
res.json(template);
} catch (ex) {
res.json(ex);
}
});
router.put("/template/:id", async (req, res, next) => {
try {
const id = req.params.id;
const { name, description, template, subject } = req.body;
const temp = await models.EmailTemplate.update(
{
name,
description,
template,
subject
},
{
where: { id }
}
);
res.json(temp);
} catch (ex) {
res.json(ex);
}
});
router.delete("/template/:id", async (req, res, next) => {
try {
const id = req.params.id;
await models.EmailTemplate.destroy({ where: { id } });
res.json({});
} catch (ex) {
res.json(ex);
}
});
router.post("/send", async (req, res, next) => {
try {
const { template, variables, to, subject, from } = req.body;
let html = template;
Object.keys(variables).forEach(variable => {
html = html.replace(`[[${variable}]]`, variables[variable]);
});
const msg = {
to,
from,
subject,
html
};
sgMail.send(msg);
res.json({});
} catch (ex) {
res.json(ex);
}
});
module.exports = router;

These are all the routes for saving our templates and send email with the template. The GET templates route get all the templates we saved. The templates/:id route gets the template by ID. We use findAll with where statement id = {id} to get the results and only get the first one to get by ID.

The POST template route create our template with the create function. The PUR template route uses the update function to update the entry found by looking up by ID. The second argument has our select condition. The DELETE template route deletes by looking up the entry by ID to delete with the destroy function.

The send route calls the SendGrid API to send the email with the variables declared in the email template filled in with the values set by the user. The request body has the variables field to send the variables with the values, where the key is the variable name and the value has the value.

Sequelize provides the create , findAll , update and destroy functions as part of the model.

In app.js , we replace the existing code with:

require('dotenv').config();
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const cors = require("cors");
const indexRouter = require("./routes/index");
const emailRouter = require("./routes/email");
const app = express();// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());
app.use("/", indexRouter);
app.use("/email", emailRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render("error");
});
module.exports = app;

We enabled CORS by adding:

app.use(cors());

And we added our routes by adding:

const emailRouter = require("./routes/email");
app.use("/email", emailRouter);

This finishes the back end of our email app.

Front End

Next, we move on to the front end. We start a new React project in the project folder by running npx create-react-app frontend .

Then we need to install some packages. We need Bootstrap for styling, MobX for state management, Axios for making HTTP requests, Formik and Yup for form value handling and form validation respectively, and React Router for routing URLs to our pages.

To install the packages, run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup .

After all the packages are installed, we can start building the app. First we replace the existing code of App.js with our code:

import React from "react";
import HomePage from "./HomePage";
import { Router, Route } from "react-router-dom";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import TopBar from "./TopBar";
import EmailPage from "./EmailPage";
import { EmailTemplateStore } from "./store";
const history = createHistory();
const emailTemplateStore = new EmailTemplateStore();
function App() {
return (
<div className="App">
<Router history={history}>
<TopBar />
<Route
path="/"
exact
component={props => (
<HomePage {...props} emailTemplateStore={emailTemplateStore} />
)}
/>
<Route
path="/email/:id"
exact
component={props => (
<EmailPage {...props} emailTemplateStore={emailTemplateStore} />
)}
/>
</Router>
</div>
);
}
export default App;

This is the entry component of our app and it contains the routes that we will add. The routes have the emailTemplateStore , which is a MobX store that we will create.

In App.css , replace the existing code with:

.page {
padding: 20px;
}

to add some padding to our pages.

Next, we create a form for adding and editing our email templates. Create a file called EmailForm.js in the src folder and add:

import React from "react";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { observer } from "mobx-react";
import { Formik } from "formik";
import { addTemplate, getTemplates, editTemplate } from "./request";
const schema = yup.object({
name: yup.string().required("Name is required"),
template: yup.string().required("Template is required"),
subject: yup.string().required("Subject is required")
});
function EmailForm({ emailTemplateStore, edit, onSave, template }) {
const handleSubmit = async evt => {
const isValid = await schema.validate(evt);
if (!isValid) {
return;
}
if (!edit) {
await addTemplate(evt);
} else {
await editTemplate(evt);
}
getAllTemplates();
};
const getAllTemplates = async () => {
const response = await getTemplates();
emailTemplateStore.setTemplates(response.data);
onSave();
};
return (
<>
<Formik
validationSchema={schema}
onSubmit={handleSubmit}
initialValues={edit ? template : {}}
>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors
}) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group as={Col} md="12" controlId="name">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
name="name"
placeholder="Name"
value={values.name || ""}
onChange={handleChange}
isInvalid={touched.name && errors.name}
/>
<Form.Control.Feedback type="invalid">
{errors.name}
</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="12" controlId="description">
<Form.Label>Description</Form.Label>
<Form.Control
type="text"
name="description"
placeholder="Description"
value={values.description || ""}
onChange={handleChange}
isInvalid={touched.description && errors.description}
/>
<Form.Control.Feedback type="invalid">
{errors.description}
</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="12" controlId="subject">
<Form.Label>Subject</Form.Label>
<Form.Control
type="text"
name="subject"
placeholder="Subject"
value={values.subject || ""}
onChange={handleChange}
isInvalid={touched.subject && errors.subject}
/>
<Form.Control.Feedback type="invalid">
{errors.subject}
</Form.Control.Feedback>
</Form.Group>
<Form.Group as={Col} md="12" controlId="template">
<Form.Label>
Template - Enter Template in HTML, Put Variables Between
Double Brackets.
</Form.Label>
<Form.Control
as="textarea"
rows="20"
name="template"
placeholder="Template"
value={values.template || ""}
onChange={handleChange}
isInvalid={touched.template && errors.template}
/>
<Form.Control.Feedback type="invalid">
{errors.template}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit" style={{ marginRight: 10 }}>
Save
</Button>
<Button type="button">Cancel</Button>
</Form>
)}
</Formik>
</>
);
}
export default observer(EmailForm);

This is the form in which we enter the email template. We have the name and template field as required fields and descriptions as an optional field. We use Formik to automatically update the form values and populate them in the evt parameter of the handleSubmit function. This saves us a lot of work by eliminating the need for writingonChange handlers for each field ourselves.

For form validation, we define the schema object made with yup and pass it into the Formik component. The form validation happens automatically and errors are displayed as soon as invalid values are entered in the Form.Control.Feedback component.

The Form component is provided by React Boostrap. The edit prop will tell if we set the initialialValues in the Formik component. We only set it to the template prop if we edit is true because only then we have something to edit.

The handleSubmit function is called when the form is submitted, it has the values of all the form fields. We call schema.validate to validate the form values against the schema before submitting it. If they’re valid, then we call addTemplate or editTemplate depending if you want to add or edit the template. The edit will tell them apart. If it’s successful, we call getAllTemplates to get all the templates and put them in our store.

We wrap observer outside the EmailForm component to get the latest values from our MobX store as soon as it’s updated.

Next, we add a page for sending emails. Add a file called EmailPage.js in the src folder and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { getTemplate, sendEmail } from "./request";
import * as yup from "yup";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Formik } from "formik";
function EmailPage({ match: { params } }) {
const [template, setTemplate] = useState({});
const [schema, setSchema] = useState(yup.object({}));
const [variables, setVariables] = useState([]);
const [initialized, setInitialized] = useState(false);
const handleSubmit = async evt => {
const isValid = await schema.validate(evt);
if (!isValid) {
return;
}
let data = { variables: {} };
data.template = evt.template;
variables.forEach(v => {
const variable = v.replace("[[", "").replace("]]", "");
data.variables[variable] = evt[variable];
});
data.to = evt.to;
data.from = evt.from;
data.subject = evt.subject;
await sendEmail(data);
alert("Email sent");
};
const getSingleTemplate = async () => {
const response = await getTemplate(params.id);
setTemplate(response.data);
const placeholders = response.data.template.match(/\[\[(.*?)\]\]/g);
setVariables(placeholders);
let schemaObj = {
to: yup.string().required("This field is required"),
from: yup.string().required("This field is required"),
subject: yup.string().required("This field is required")
};
placeholders.forEach(p => {
p = p.replace("[[", "").replace("]]", "");
schemaObj[p] = yup.string().required("This field is required");
});
let newSchema = yup.object(schemaObj);
setSchema(newSchema);
setInitialized(true);
};
useEffect(() => {
if (!initialized) {
getSingleTemplate();
}
});
return (
<div className="page">
<h1 className="text-center">Send Email</h1>
<Formik
validationSchema={schema}
onSubmit={handleSubmit}
enableReinitialize={true}
initialValues={template}
>
{({
handleSubmit,
handleChange,
handleBlur,
values,
touched,
isInvalid,
errors
}) => (
<Form noValidate onSubmit={handleSubmit}>
{variables.map((v, i) => {
const variable = v.replace("[[", "").replace("]]", "");
return (
<Form.Row key={i}>
<Form.Group as={Col} md="12" controlId="name">
<Form.Label>Variable - {variable}</Form.Label>
<Form.Control
type="text"
name={variable}
value={values[variable] || ""}
onChange={handleChange}
isInvalid={touched[variable] && errors[variable]}
/>
<Form.Control.Feedback type="invalid">
{errors[variable]}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
);
})}
<Form.Row>
<Form.Group as={Col} md="12" controlId="from">
<Form.Label>From Email</Form.Label>
<Form.Control
type="text"
name="from"
placeholder="From Email"
value={values.from || ""}
onChange={handleChange}
isInvalid={touched.from && errors.from}
/>
<Form.Control.Feedback type="invalid">
{errors.from}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="to">
<Form.Label>To Email</Form.Label>
<Form.Control
type="text"
name="to"
placeholder="To Email"
value={values.to || ""}
onChange={handleChange}
isInvalid={touched.to && errors.to}
/>
<Form.Control.Feedback type="invalid">
{errors.to}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="subject">
<Form.Label>Subject</Form.Label>
<Form.Control
type="text"
name="subject"
placeholder="Subject"
value={values.subject || ""}
onChange={handleChange}
isInvalid={touched.subject && errors.subject}
/>
<Form.Control.Feedback type="invalid">
{errors.subject}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Form.Row>
<Form.Group as={Col} md="12" controlId="template">
<Form.Label>Template</Form.Label>
<Form.Control
as="textarea"
rows="20"
name="template"
placeholder="Template"
value={values.template || ""}
onChange={handleChange}
isInvalid={touched.template && errors.template}
readOnly
/>
<Form.Control.Feedback type="invalid">
{errors.template}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
<Button type="submit" style={{ marginRight: 10 }}>
Send
</Button>
</Form>
)}
</Formik>
</div>
);
}
export default withRouter(EmailPage);

In this component, we get the email template by ID and we extract the variables from the template text in the getSingleTemplate function. We also create a validate schema with Yup with:

let schemaObj = {
to: yup.string().required("This field is required"),
from: yup.string().required("This field is required"),
subject: yup.string().required("This field is required")
};
placeholders.forEach(p => {
p = p.replace("[[", "").replace("]]", "");
schemaObj[p] = yup.string().required("This field is required");
});
let newSchema = yup.object(schemaObj);

To build the Yup schema, we first add the static fields to , from and subject for validation. Then we loop through the keys of the variable field of the response.data object and build the Yup form validation schema dynamically by removing the brackets then using each placeholder entry as a key.

In the Formik component, we put the prop:

enableReinitialize={true}

so that we can populate the template text at the form field with name prop template .

In the Form component, we have:

{variables.map((v, i) => {
const variable = v.replace("[[", "").replace("]]", "");
return (
<Form.Row key={i}>
<Form.Group as={Col} md="12" controlId="name">
<Form.Label>Variable - {variable}</Form.Label>
<Form.Control
type="text"
name={variable}
value={values[variable] || ""}
onChange={handleChange}
isInvalid={touched[variable] && errors[variable]}
/>
<Form.Control.Feedback type="invalid">
{errors[variable]}
</Form.Control.Feedback>
</Form.Group>
</Form.Row>
);
})}

to dynamically loop through the variables that we set in the getSingleTemplate function and render the form field with the form validation message. We set the name prop to the variable so that the right message will be displayed. A variable in the same as a placeholder with the brackets removed.

We need to wrap the withRouter function outside EmailPage so that we can get the match prop so that we get the ID of the email template from the URL.

Next, we build the home page, which will have a table for displaying the list of templates saved and have buttons in each table row for letting users send emails with the template or delete or edit the template. There will also be a button to let the user add an email template.

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import EmailForm from "./EmailForm";
import Modal from "react-bootstrap/Modal";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Table from "react-bootstrap/Table";
import { observer } from "mobx-react";
import { getTemplates, deleteTemplate } from "./request";
function HomePage({ emailTemplateStore, history }) {
const [openAddModal, setOpenAddModal] = useState(false);
const [openEditModal, setOpenEditModal] = useState(false);
const [initialized, setInitialized] = useState(false);
const [template, setTemplate] = useState([]);
const openAddTemplateModal = () => {
setOpenAddModal(true);
};
const closeAddModal = () => {
setOpenAddModal(false);
setOpenEditModal(false);
};
const cancelAddModal = () => {
setOpenAddModal(false);
};
const cancelEditModal = () => {
setOpenEditModal(false);
};
const getAllTemplates = async () => {
const response = await getTemplates();
emailTemplateStore.setTemplates(response.data);
setInitialized(true);
};
const editTemplate = template => {
setTemplate(template);
setOpenEditModal(true);
};
const onSave = () => {
cancelAddModal();
cancelEditModal();
};
const deleteSelectedTemplate = async id => {
await deleteTemplate(id);
getAllTemplates();
};
const sendEmail = template => {
history.push(`/email/${template.id}`);
};
useEffect(() => {
if (!initialized) {
getAllTemplates();
}
});
return (
<div className="page">
<h1 className="text-center">Templates</h1>
<ButtonToolbar onClick={openAddTemplateModal}>
<Button variant="primary">Add Template</Button>
</ButtonToolbar>
<Modal show={openAddModal} onHide={closeAddModal}>
<Modal.Header closeButton>
<Modal.Title>Add Template</Modal.Title>
</Modal.Header>
<Modal.Body>
<EmailForm
onSave={onSave.bind(this)}
cancelModal={cancelAddModal.bind(this)}
emailTemplateStore={emailTemplateStore}
/>
</Modal.Body>
</Modal>
<Modal show={openEditModal} onHide={cancelEditModal}>
<Modal.Header closeButton>
<Modal.Title>Edit Template</Modal.Title>
</Modal.Header>
<Modal.Body>
<EmailForm
edit={true}
template={template}
onSave={onSave.bind(this)}
cancelModal={cancelEditModal.bind(this)}
emailTemplateStore={emailTemplateStore}
/>
</Modal.Body>
</Modal>
<br />
<Table striped bordered hover>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Subject</th>
<th>Send Email</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{emailTemplateStore.templates.map(t => (
<tr key={t.id}>
<td>{t.name}</td>
<td>{t.description}</td>
<td>{t.subject}</td>
<td>
<Button
variant="outline-primary"
onClick={sendEmail.bind(this, t)}
>
Send Email
</Button>
</td>
<td>
<Button
variant="outline-primary"
onClick={editTemplate.bind(this, t)}
>
Edit
</Button>
</td>
<td>
<Button
variant="outline-primary"
onClick={deleteSelectedTemplate.bind(this, t.id)}
>
Delete
</Button>
</td>
</tr>
))}
</tbody>
</Table>
</div>
);
}
export default withRouter(observer(HomePage));

We have the Table provided by React Boostrap. Again, we wrap the withRouter function outside the HomePage component at the bottom so that we get the history object in our props, which let us call history.push to redirect to our email page.

We have the openAddTemplateModal, closeAddModal, cancelAddModal, cancelEditModal , and onSave function to open or close the add or edit modals. onSave is used by EmailForm component by passing it in as a prop to EmailForm . We have a getAllTemplates function to get the templates we saved. We use the useEffect ‘s callback function to get the templates on the first load by checking for the initialized variable, if it’s false then getAllTemplates load and set initialized to false so it won’t load again.

The Modal component is provided by React Bootstrap. In both the add and edit modals, we use the same EmailForm for saving templates. We can tell whether the user is adding or editing by using the edit prop.

Next, create a file called requests.js and add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const getTemplates = () => axios.get(`${APIURL}/email/templates`);export const getTemplate = id => axios.get(`${APIURL}/email/template/${id}`);export const addTemplate = data => axios.post(`${APIURL}/email/template`, data);export const editTemplate = data =>
axios.put(`${APIURL}/email/template/${data.id}`, data);
export const deleteTemplate = id =>
axios.delete(`${APIURL}/email/template/${id}`);
export const sendEmail = data => axios.post(`${APIURL}/email/send`, data);

to let users make HTTP requests to our back end.

Then we create store.js in the src and put:

import { observable, action, decorate } from "mobx";class EmailTemplateStore {
templates = [];
setTemplates(templates) {
this.templates = templates;
}
}
EmailTemplateStore = decorate(EmailTemplateStore, {
templates: observable,
setTemplates: action
});
export { EmailTemplateStore };

to let us store the templates array in a central localization for easy access by all components. We pass an instance of this in our components via the emailTemplateStore prop of the components to call the setTemplates function to set the templates and access the templates by using emailTemplateStore.templates . That’s why we wrap observable function around our components. We need the latest values as they are updated here. Also, we designated templates as observable in the decorate function to allow templates field to have the latest value whenever it is accessed. setTemplates is designated as action so that we can call it to manipulate the store.

Next, create TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import { withRouter } from "react-router-dom";
function TopBar({ location }) {
return (
<Navbar bg="primary" expand="lg" variant="dark">
<Navbar.Brand href="#home">Email App</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/" active={location.pathname == "/"}>
Home
</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
);
}
export default withRouter(TopBar);

to create a top bar by using the Navbar component provided by React Boostrap. We check the pathname to highlight the right links by setting the active prop.

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Email App</title>
<link
rel="stylesheet"
href="https://stackpath.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 add the Bootstrap CSS and change the title of the app.

Once all that is done go into the backend folder and run npm start to start the back end and go into the frontend folder and run the same command. Answer yes if you’re asked whether you want to run the app from a different port.

In the end, we get:

Learn the web's most important programming language.

John Au-Yeung

Written by

JavaScript in Plain English
Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade