OceanofPDF - Com The React Mega-Tutorial Learn Front End Development With React by Building A Complete Project Step-By-Step - Miguel Grinberg
OceanofPDF - Com The React Mega-Tutorial Learn Front End Development With React by Building A Complete Project Step-By-Step - Miguel Grinberg
Contents
Preface
Prerequisites
How to Work with the Example Code
Acknowledgements
Modern JavaScript
ES5 vs. ES6
Summary of Recent JavaScript Features
Hello, React!
Installing Node.js
Creating a Starter React Project
Installing Third-Party Dependencies
Application Structure
A Basic React Application
Dynamic Rendering
Chapter Summary
Working with Components
User Interface Components
The Container Component
Adding a Header Component
Adding a Sidebar
Building Reusable Components
Components with Props
Chapter Summary
Routing and Page Navigation
Creating Page Components
Implementing Links
Pages with Dynamic Parameters
Chapter Summary
Connecting to a Back End
Running the Microblog API Back End
Using State Variables
Side Effect Functions
Rendering Blog Posts
Displaying Relative Times
Chapter Summary
Building an API Client
What is an API Client?
A Simple Client Class for Microblog API
Sharing the API Client through a Context
The User Profile Page
Making Components Reusable Through Props
Pagination
Chapter Summary
Forms and Validation
Introduction to Forms with React-Bootstrap
A Reusable Form Input Field
The Login Form
Controlled and Uncontrolled Components
Accessing Components through DOM References
Client-Side Field Validation
The User Registration Form
Form Submission and Server-Side Field Validation
Flashing Messages to the User
Chapter Summary
Authentication
Enabling Back End Authentication
Authentication in the API Client
A User Context and Hook
Implementing Private Routes
Public Routes
Routing Public and Private Pages
Hooking Up the Login Form
User Information in the Header
Handling Refresh Tokens
Chapter Summary
Application Features
Submitting Blog Posts
User Page Actions
Changing the Password
Password Resets
Chapter Summary
Memoization
The React Rendering Algorithm
Unnecessary Renders
Memoizing Components
Render Loops
Memoizing Functions and Objects
Chapter Summary
Automated Testing
The Purpose of Automated Testing
Testing React Applications with Jest
Renders, Queries and Assertions
Testing Individual Components
Using Fake Timers
Mocking API Calls
Chapter Summary
Production Builds
Development vs. Production Builds
Generating React Production Builds
Deploying the Application
Production Deployment with Docker
Chapter Summary
Next Steps
OceanofPDF.com
Preface
Welcome to the React Mega-Tutorial! In this book I will share
my experience in developing real-world, non-trivial front end
applications using the React library and a handful of related
packages.
Prerequisites
To make the most of this book, you need to have basic
experience writing applications in JavaScript. If you have
created websites that have bits of interactive behavior using
vanilla JavaScript, or maybe jQuery, then you should be fine.
You are welcome to use the editor that makes you most
productive. At the time I'm writing this, Visual Studio Code is
the favorite editor of many JavaScript developers, but these
things tend to change over time, and I've taken special care
to not introduce any dependencies to this or other editors.
To test your application as you develop it, you may use your
favorite web browser. If you need a recommendation, many
React developers prefer Chrome or Firefox, because they are
the two browsers that support the React Developer Tools
plugin, which can sometimes be useful in debugging React
applications.
Acknowledgements
Writing a book is a huge task that would have been
impossible for me to complete if I didn't have the support
and encouragement of my family. My deepest gratitude
goes to my wife and my children, for allowing me to spend
long hours after a day of work, hunched over the computer
to bring this project to life. My dog Hazel gets an honorable
mention, for reminding me that it is important to take a
break once in a while, go out for a walk (with her, of course),
and breathe some fresh air.
OceanofPDF.com
Modern JavaScript
The JavaScript language has evolved significantly in the last
few years, but because browsers have been slow in
adopting these changes a lot of people have not kept up
with the language. React encourages developers to use
modern JavaScript, so this chapter gives you an overview of
the newest features of the language.
You may have heard the terms "ES5" and "ES6" in the
context of JavaScript language versions. These refer to the
5th and 6th editions of the ECMAScript standard
respectively. The ES5 version of the language, which was
released in 2009, is currently considered a baseline
implementation, with wide support across desktop and
mobile devices. ES6, released in 2015, introduces significant
improvements over ES5, and remains backwards compatible
with it. Since the release of ES6, ECMA has been making
yearly revisions to the standard, which continue to improve
and modernize the language. In many contexts, the "ES6"
denomination is loosely used for all the improvements
brought to the language after ES5, and not strictly those
from the ES6 specification.
Semicolons
function f() {
console.log('this is f'); // <-- semicolon here
} // <-- but not here
const f = () => {
console.log('this is f');
}; // <-- this is an assignment, so a semicolon is used
Trailing Commas
const myArray = [
1,
3,
5,
];
const myObject = {
name: 'susan',
age: 20,
};
The commas after the last elements of the array and object
might seem like a syntax error at first, but they are valid. In
fact, JavaScript is not unique in allowing this, as most other
languages support trailing commas as well.
There are two benefits that result from this practice. The
most important one is that if you need to reorder the
elements, you can just move the lines up and down, without
worrying about having to fix the commas. The other one is
that when you need to add an element at the end, you do
not need to go up to the previous line to add the trailing
comma.
A module can have only one default export, but it can also
export additional things. Here is an extension of the above
cool.js module with a couple of exported constants (you'll
learn more about constants in the next section):
let a;
let a = 1;
console.log(c); // 3
c = 4; // error
d.push(4); // allowed
console.log(d) // [1, 2, 3, 4]
String Interpolation
For-Of Loops
Arrow Functions
function mult(x, y) {
const result = x * y;
return result;
}
mult(2, 3); // 6
mult(2, 3); // 6
square(2); // 4
Promises
fetch('http://example.com/data.json')
.then(r => r.json())
.then(data => console.log(data));
fetch('http://example.com/data.json')
.then(r => r.json())
.then(data => console.log(data))
.catch(error => console.log(`Error: ${error}`));
Spread Operator
Let's say you have an array with some numbers, and you
want to find the smallest of them. The traditional way to do
this would require a for-loop. With the spread operator, you
can leverage the Math.min() function, which takes a variable
list of arguments:
Here, the collision that occurs when having two values for
the age key is resolved by using the version that appears
last.
See the Spread syntax reference for more details.
Destructuring Assignments
Classes
class User {
constructor(name, age, active) { // constructor
this.name = name;
this.age = age;
this.active = active;
}
JSX
const myTable = (
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Susan</td>
<td>20</td>
</tr>
<tr>
<td>John</td>
<td>45</td>
</tr>
</table>
);
OceanofPDF.com
Hello, React!
In this chapter you will take your first steps as a React developer.
When you reach the end you will have a first version of a
microblogging application running on your computer!
Installing Node.js
While React is a front end framework that runs in the browser,
some related utilities are designed to run on your own computer,
using the Node.js engine. The project that creates a brand-new
React project, for example, runs on Node.
node -v
The npx command comes with Node.js, along with npm which you
may be more familiar with. Its purpose is to execute Node.js
packages. The first argument to npx is the package to execute,
which is called create-react-app. Additional arguments are passed
to this package once it runs. The Create React App package
takes the name of the React project to create as an argument.
You may have noticed that you did not need to install create-
react-app prior to running it. The nice thing about npx is that it
downloads and runs the requested package on the fly.
Let's have a look at the newly created project. First change into
the react-microblog directory:
cd react-microblog
Depending on the version of Create React App that you use the
contents of the project may not match mine exactly, but you
should expect to have the following files and directories:
Before you continue, you'd want to make sure that the starter
project works well. To start the React development web server,
run the following command:
npm start
The application that you are going to build requires a few more
packages, so this is a good time to get them installed. Run the
following command from the top-level directory of the project:
npm install bootstrap react-bootstrap react-router-dom serve
You will become familiar with these packages as you work on the
project.
Application Structure
On the one side, generating a starter application with Create
React App saves a lot of time, but on the other you end up with
an application that has some unnecessary components that need
to be removed before embarking on a new project. In this section
you will learn about the general structure of the React
application you just created, and while you are at it, you will
remove some cruft.
Before you continue, make sure that you have the npm start
command running in a terminal window, and a tab in your
browser open on the application. Some of the changes you are
going to make soon will cause the application to temporarily
break, so this is a great opportunity for you to experience the
way the React development server watches your work and
notifies you of errors.
<meta
name="description"
content="Web site created using create-react-app"
/>
A few lines below the meta description tag, you will find the page
title:
<title>React App</title>
The short_name and name keys are set to generic values, similar to
those in the index page. You can update these to "Microblog" and
"React Microblog" respectively:
The rest of the file defines icons of various sizes, and theme
colors for the application. You can leave these settings as they
are.
The Icon Files
The code in index.js has the entry point for the React rendering
engine. The code in this file may vary depending on what version
of React and Create React App you use, but in general you
should expect to have a main section with a call to render the
application, similar to the following:
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
You may notice that there are some strange things in this source
file. First, JSX is used in the argument to the render() function.
Also, a file with extension .css is being imported at the top,
which does not make a lot of sense in terms of the JavaScript
language. Keep in mind that all source files in a React project go
through a conversion step before they reach the browser, where
any extensions to the JavaScript language are processed and
converted into valid code.
The App symbol is imported from the App.js source file. This is the
top-level component of the application. You will learn more about
components shortly, but for now, consider that App represents a
hierarchical collection of elements that represent the entire
application. A React component is considered a super-powered
HTML element, and for that reason it is rendered using angle
brackets, such as a standard HTML element. When looking at a
JSX tree, you can tell the difference between native HTML
elements and React components because the former use
lowercase letters and the latter use CamelCase.
There is only one change that you are going to make in index.js.
As you recall, earlier you installed a few third-party packages.
One of those packages was bootstrap, which is a CSS framework.
To add this library to the project, its CSS file must be imported.
Insert the following import statement right above the line in
which the index.css file is imported:
Listing 4 src/index.js: Add the bootstrap framework
import 'bootstrap/dist/css/bootstrap.min.css';
As soon as you save the file, you will notice that the look of the
application in the browser changes slightly. This is because the
bootstrap styles introduce some minor visual differences on the
rendered page.
The src directory has two files called logo.svg and App.css that
are not going to be used by the application, so you are going to
delete them.
Why are these files unnecessary? The logo.svg file is the rotating
React logo that appears in the center of the page in the starter
application. This is a nice graphic for the demo application, but it
has no place in your own application. The App.css file stores CSS
definitions for the top-level application component, but the
project also has an index.css file with application-wide styles, so
there is some redundancy in having two CSS files. For this
project I have decided to maintain the entire collection of CSS
styles in the index.css file.
Right after you delete these two files the npm start process is
going to get upset and spew a few error messages on your
terminal and on the browser. This is because the App.js source
file references the two files that are now gone.
In spite of the errors, npm start will continue to monitor your files
and wait for you to fix the errors to restart the application.
Note the parenthesis that enclose the JSX that the function
returns. Due to the strange and somewhat unpredictable
semicolon rules in JavaScript, the opening parenthesis needs to
be in the same line as the return keyword, to prevent the
JavaScript compiler from inserting a virtual semicolon on that
line. The opening parenthesis tells the compiler that the
expression continues in the next line.
Dynamic Rendering
Rendering chunks of HTML returned by component functions is
nice, but insufficient for the vast majority of applications you
may want to build. A key feature that most applications need is
the ability to render content that is dynamic. For example, the
Microblog application you are going to build needs to render blog
posts that are not known in advance, and will be retrieved from a
back end service.
Given that there is no back end yet, let's create a mock blog post
that can be rendered to the page. Replace the code in App.js
with the following:
return (
<>
<h1>Microblog</h1>
<p>
<b>{post.author.username}</b> — {post.timestamp}
<br />
{post.text}
</p>
</>
);
}
The <br /> notation might also look strange if you are used to
often see <br> to insert line breaks in HTML pages. JSX requires a
strict XML syntax, so all elements must be properly closed.
return (
<>{data}</>
);
}
The output of this component is going to be onetwothree, which
isn't very useful, as there is no separation between the items
and no way to add HTML markup for each element of the array.
The way to make this work is to use the map() method of the Array
class to transform each element into the desired JSX expression.
The map() method takes a function as an argument. This function
is called once for each of the elements in the array, with the
element as its only argument. The return values for all these
calls are collected and returned by map() as a new array, and this
becomes the render output of the component.
return (
<ul>
{data.map(element => {
return <li>{element}</li>
})}
</ul>
);
}
If you aren't familiar with the Array.map() method, the inner return
statement in this last example may seem strange. Remember
that map() takes a function as its argument, so this inner return is
returning values back to map(), which is the caller of the inner
function.
<ul><li>one</li><li>two</li><li>three</li></ul>
return (
<>
<h1>Microblog</h1>
{posts.map(post => {
return (
<p>
<b>{post.author.username}</b> —
{post.timestamp}
<br />
{post.text}
</p>
);
})}
</>
);
}
You can see how the list of posts looks in the browser in Figure 4.
Figure 4 Render a list of blog posts
This looks great, but there is a hidden problem. If you look at the
browser's debugging console, you will see a warning message.
Conditional Rendering
The last templating trick you are going to learn gives the React
application the ability to render parts of the JSX tree only when
certain condition is true.
return (
<>
<ul>{data.map(element => <li>{element}</li>)}</ul>
{data.length === 0 &&
<p>There is nothing to show here.</p>
}
</>
);
}
return (
<>
{data.length === 0 ?
<p>There is nothing to show here.</p>
:
<ul>{data.map(element => <li>{element}</li>)}</ul>
}
</>
);
}
return (
<>
<h1>Microblog</h1>
{posts.length === 0 ?
<p>There are no blog posts.</p>
:
posts.map(post => {
return (
<p key={post.id}>
<b>{post.author.username}</b> —
{post.timestamp}
<br />
{post.text}
</p>
);
})
}
</>
);
}
With this version of the application you can comment out the
posts constant and put an empty array in its place, and the page
will automatically change to show the message in the "else"
part, as seen in Figure 5.
Figure 5 Render a warning that the post list is empty
OceanofPDF.com
Working with Components
In Chapter 2, you wrote your first React component. In this
chapter you will delve deeper into React as you learn how to
create robust applications by combining and reusing
components, not only your own but also some imported from
third-party libraries.
return (
<Container fluid className="App">
... // <-- no changes to JSX content
</Container>
);
}
Save the changes and note how the Container component adds a
nice margin to the page. Also try resizing the browser window to
see how the container resizes with it. You may notice that the
font sizes slightly increase as you make the window larger.
Bootstrap styles elements of the page differently for different
screen sizes, a technique that helps make websites look their
best on phones, tablets, laptops and desktop computers. Figure
6 shows how the fluid container looks.
Figure 6 Fluid container from React-Bootstrap
The project as created by Create React App does not provide any
guidance on how to structure the source code. I find it useful to
put all the custom components of the application in subdirectory
dedicated to them. Create this directory from your terminal:
mkdir src/components
Below you can see the Header component, which uses Navbar and
Container from React-Bootstrap. Copy this code into
src/components/Header.js.
return (
<Container fluid className="App">
<Header />
<Container>
{posts.length === 0 ?
...
}
</Container>
</Container>
);
}
If you look at the header carefully, you will notice that it does not
extend all the way to the left and right borders of the window,
there is actually a small white margin on each side. Looking at
the styles in the page with the browser's inspector I determined
that the top-level <div>, which is rendered by the fluid Container,
has non-zero padding. The solution to address this minor
cosmetic annoyance is to override the padding for this
component, which has the App class name.
Another aspect of the styling of the header that I don't quite like
is that there is no separation between the bottom of the header
and the content area below it. A slightly darker border line there
would make the separation more clearly visible.
The src/index.css file is where you will enter all the custom styles
needed by the application. Open this file in your editor and
replace all of its contents, which were added by Create React
App and are not useful to this application, with the following:
Listing 13 src/index.css: Custom styles for App and Header
components
.App {
padding: 0;
}
.Header {
border-bottom: 1px solid #ddd;
}
Adding a Sidebar
Another common user interface component in many web
applications is a sidebar. In Microblog, the sidebar will offer
navigation links to switch between the "feed" page, which shows
all the blog posts from followed users, and the "explore" page,
which shows blog posts from all users.
Now the sidebar needs to be added to the page, to the left of the
content area. When needing to position two or more components
side by side, the ideal layout tool is a horizontal stack. Below you
can see the updated App component with a sidebar.
Listing 15 src/App.js: Add a sidebar
import Container from 'react-bootstrap/Container';
import Stack from 'react-bootstrap/Stack';
import Header from './components/Header';
import Sidebar from './components/Sidebar';
return (
<Container fluid className="App">
<Header />
<Container>
<Stack direction="horizontal">
<Sidebar />
<Container>
{posts.length === 0 ?
... // <-- no changes to render loop
}
</Container>
</Stack>
</Container>
</Container>
);
The Stack component was added with the direction attribute set
to horizontal, which is necessary because the default for this
component is to lay components out vertically. The stack has two
children, the sidebar, and an inner Container with the fake blog
posts. These two components will now appear side by side, as
you see in Figure 9.
Figure 9 Sidebar
The sidebar needs some styling work to look its best. Add the
following CSS definitions to src/index.css.
.Sidebar {
width: 120px;
margin: 5px;
position: sticky;
top: 62px;
align-self: flex-start;
align-items: start;
}
.Sidebar .nav-item {
width: 100%;
}
.Sidebar a {
color: #444;
}
.Sidebar a:hover {
background-color: #eee;
}
.Sidebar a:visited {
color: #444;
}
You may be wondering where does the .nav-item class come from
in the above CSS definitions. This is a class defined by the
Bootstrap library, and used by the Nav.Link component from
React-Bootstrap. As mentioned above, opening the browser's
developer console and looking at the rendered elements on the
page is often the easiest way to find what classes are used and
are potential targets for redefining the look of some elements.
The CSS definition for .nav-item sets the width of the <Nav.Item>
elements to 100%, which means that they will have the
maximum width of the sidebar instead of the width of their text.
This is done so that the hover style, which alters the background,
shows a full bar regardless of the length of the text in the link.
With the styling updates, the sidebar looks much better. If you
hover the mouse pointer over an item, its background color
changes to highlight it. See the current state of the sidebar in
Figure 10.
return (
<>
{posts.length === 0 ?
<p>There are no blog posts.</p>
:
posts.map(post => {
return (
<p key={post.id}>
<b>{post.author.username}</b> —
{post.timestamp}
<br />
{post.text}
</p>
);
})
}
</>
);
}
With this new component in the project, the posts fake blog post
array in App can be removed, and the loop that renders it can be
replaced with <Posts />. Here is the updated version of App:
This new component, which I'm going to call Body, has to be very
generic enough to be able to render the main page content with
or without a sidebar. It is the first in this application that needs to
accept input arguments, which in React are called props.
<Body sidebar={true}>
<Posts />
</Body>
<Body sidebar={false}>
<h1>Login</h1>
<form>
...
</form>
</Body>
<Body>
<h1>Login</h1>
<form>
...
</form>
</Body>
You are not going to see any changes to how the application
looks in the browser, but this is a very robust and scalable
refactor that is ready to be expanded to support multiple pages
and the routing between them.
Chapter Summary
To avoid reinventing the wheel, use a user interface
component library such as React-Bootstrap.
Add a top-level container component that provides sensible
margins on all screen sizes.
To keep your code better organized, create a subdirectory for
application components.
To maximize code reuse, do not add too much to a single
component, and instead split the work across several
components, each having a single purpose.
Use props to create components that are reusable.
OceanofPDF.com
Routing and Page
Navigation
React is a Single-Page Application (SPA) framework, which means
that from the point of view of the browser, only one web page is
ever downloaded. Once that page is active, all the application
state changes will happen through JavaScript events, without the
browser having to fetch new web pages from the server. How
then, can the application support page navigation?
The default page for this application is going to be the page that
displays the post feed for the user. Let's move the Body
component, which is currently in App, to a new FeedPage
component, stored in src/pages/FeedPage.js.
Even though the application isn't ready to have a login page yet,
also create a placeholder for the LoginPage component, so that
you can later test navigation between three different pages.
With this version of the application, the address bar, Back and
Forward buttons of your browser are connected to the
application, and routing is functional. If you type
http://localhost:3000/login in the address bar of your browser,
the application will load and automatically render the login page
placeholder added earlier. The same will occur if you use
/explore in the path portion of the URL.
The two links in the sidebar are standard browser links that are
not connected to React-Router yet, so they trigger a full page
reload. Ignoring the inefficiency this causes (which will soon be
fixed), they should allow you to switch between the feed and
explore pages.
After you've moved between the three pages a few times, try the
Back and Forward buttons in your browser to see how React-
Router makes the SPA behave like a normal multipage website.
Implementing Links
As noted above, the two links in the sidebar are working in the
sense that they allow you to navigate between pages, but they
are inefficient, because they are standard browser links that
reload the entire React application every time they are clicked.
For a single-page application, the routing between pages should
be handled internally in JavaScript, without reaching the
browser's own page navigation system.
<Nav.Link href="/">Feed</Nav.Link>
Here is how the above link can be adapted to work with React-
Router's NavLink, while still being compatible with Bootstrap:
Note how the href attribute of Nav.Link has been renamed to to,
because now this component will render as React-Router's
NavLink, which uses to instead of href. With this solution, the
Bootstrap CSS classes associated with navigation links are
preserved, but they are applied to the React-Router NavLink
component.
With this new CSS definition, you can navigate between the feed
and explore pages, and whichever of the two links is active
renders with a light blue background color, as shown in Figure
11.
Figure 11 Navigation link styling
This is the first time you encounter a function that starts with the
word use. In React, "use" functions are called hooks. Hook
functions are special in React because they provide access to
application state. React includes a number of hooks, most of
which you'll encounter in later chapters. Many libraries for React
also provide their own hooks, such as the useParams() hook from
React-Router. Applications can create custom hooks as well, and
you will also learn about this later.
return (
<Body sidebar>
<h1>{username}</h1>
<p>TODO</p>
</Body>
);
}
The listing below shows the updated App component with the
user profile page.
As with the login page, the new user profile page is currently not
linked from any other part of the application, so the only way to
view it is by typing a matching URL in the browser's address bar.
Figure 12 shows the user page that corresponds to the
http://localhost:3000/user/susan URL.
Figure 12 User profile page with a dynamic parameter
Chapter Summary
Use React-Router to create client-side routes in your React
application.
Following on the idea of having nicely organized code, create
a subdirectory for page-level components.
Create each logical page of the application as a separate
page-level component that can be defined as a route.
OceanofPDF.com
Connecting to a Back End
In this chapter you are going to learn how the React application
can communicate with a back end application to request data.
While doing this, you will learn how to use the two most
important React hooks: useState() and useEffect().
Microblog API has a read me page that you can consult for
detailed installation instructions, but the following sections cover
the basic installation steps for three different installation
methods.
MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey # <-- this is the literal word "apikey"
MAIL_PASSWORD= # <-- your SendGrid API key here
MAIL_DEFAULT_SENDER= # <-- the sender email address you'd like to
use
Once the service is up and running, run the next two commands
to populate the database used by the service with some
randomly generated data:
docker-compose run --rm microblog-api bash -c "flask fake users 10"
docker-compose run --rm microblog-api bash -c "flask fake posts 100"
To check that the service is running correctly, navigate to the
http://localhost:5000 URL on your web browser. This should open
the live API documentation site.
If you are familiar with running Python applications, you can just
install and run the application directly on your computer. For this
you need to have a recent Python 3 interpreter and git installed.
flask db upgrade
flask fake users 10
flask fake posts 100
If you need to run the service on a different port, add the --port
option to the run command. For example, to use port 4000, the
command is:
If you want to deploy to the Heroku service, first make sure you
have an account on this service. A free account is sufficient for
the needs of this project.
The read me page for the Microblog API project has a "Deploy to
Heroku" button. Click this button to configure the deployment.
In the "App name" field you have to enter a name for the
deployed back end application. You will need to find a name that
hasn't been used by any other Heroku customer, so it may take a
few tries until you find a unique name that is accepted.
For the "Choose a region" dropdown you can pick a region that is
closest to where you are located, or you can leave the default
selection.
The next set of fields are for the email settings. Above you can
find the settings you have to use if you followed my
recommendation of using a SendGrid account. If you use a
different email provider, configure it as required by your service.
You can leave any additional fields set to their default values.
When you are done with the configuration, click the "Deploy
app" button. The deployment should only take a minute or two.
When the application is deployed, you should see all green check
marks, as shown in Figure 13.
How can the data be requested then? The short answer is that
this is done as a side effect, but you will learn what this means in
the next section. For now let's just say that the component has
to schedule the request to run as a background task, so that the
render process isn't held back.
After you think about this process, you will likely wonder how do
the background data retrieval function, React and the rendering
component communicate and coordinate to perform the three
steps outlined above.
The return value from the hook is an array with two elements,
which in the above example are assigned to two constants using
a destructuring assignment.
The first element of the returned array is the current value of the
state variable. Going back to the three rendering steps above,
when the component renders for the first time in step 1, this
would be the initial value assigned to the state variable, which in
this case is undefined.
The second element of the array is a setter function for the state
variable. This function must be used to update the value of the
state variable. This will be done in the background task when the
data is received. Calling the setter function with a new value is
what allows React to trigger the render that occurs in step 3
above.
The first step in the migration to use real data in the Posts
component is to replace the posts constant and the fake blog
posts with a state variable of the same name.
return (
<>
{posts === undefined ?
<Spinner animation="border" />
:
<>
... // <-- no changes to blog post JSX
</>
}
</>
);
}
The first line imports the useState() hook from the React library.
The Spinner component imported in the second line comes with
React-Bootstrap.
The return statement that looped through the blog posts has
been expanded. At the top level there is a conditional that
checks if the posts state variable is set to undefined, and in that
case a spinner component is returned. This implements step 1 of
the render process described earlier in this section, when the
data from the server isn't available yet.
If posts has a value other than undefined, then it means that this is
the second render, so in that case the previous loop logic is used
to render the data obtained from the server.
Before the front end can make a request to the server, it needs
to know what is the root URL of your back end. Depending on
your back end installation method here is how you can
determine this URL:
If you are running the back end on a local Docker, then your
root URL is http://localhost:5000. If the Docker host is
remote, then change localhost to the IP address or hostname
of your Docker host. When using a custom port, change the
5000 to your chosen port number.
If you are running the Python application directly on your
computer, then your root URL is also http://localhost:5000.
Once again, be sure to change the 5000 to the correct number
if you are using a custom port.
If you are running the back end on Heroku, then your root
URL is https://APPNAME.herokuapp.com, where APPNAME is the
application name that you selected when you deployed the
service. If you can't remember the name, you can visit your
Heroku dashboard to look it up.
To check that you have the correct root URL for your back end,
type this URL in your web browser. If the URL is correct, the
browser should automatically redirect to /docs and show you the
API documentation site. You can see how this looks in Figure 15.
REACT_APP_BASE_API_URL=http://localhost:5000
Do not include a trailing slash when you enter the back end URL.
Save the .env file with the new variable. Then stop the npm start
process with Ctrl-C and restart it, so that the environment file is
imported. The Create React App build will make the variable
accessible anywhere in the application as
process.env.REACT_APP_BASE_API_URL.
From this page you can learn that the HTTP method for this
request is GET, and that the URL path to attach after the root
server URL is /api/feed. There are some query parameters
related to pagination, but they are all optional, so there is no
need to worry about those for now.
On the right side of the page there is a web form, where you can
enter input arguments for the request and send a test request
out to the server. The only input argument this endpoint requires
is the authentication token, but it is currently not checked, so
you can click the "Send API Request" button with all the input
fields blank and see an example response from this endpoint.
This will allow you to familiarize yourself with a real response,
which should include some randomly generated users and blog
posts.
Later you will see that there are cases that require specific
values for this array, but it is best to get used to set this
argument to an empty array and then changing it only when
necessary.
The listing below shows the updated Posts component, with the
side effect function in place.
useEffect(() => {
(async () => {
const response = await fetch(BASE_API_URL + '/api/feed');
if (response.ok) {
const results = await response.json();
setPosts(results.data);
}
else {
setPosts(null);
}
})();
}, []);
return (
<>
{posts === undefined ?
<Spinner animation="border" />
:
<>
{posts === null ?
<p>Could not retrieve blog posts.</p>
:
<>
... // <-- no changes to blog post JSX
</>
}
</>
}
</>
);
}
The side effect function is defined after the posts state variable,
so that it can access the setPosts() setter function to update the
list of posts.
(async () => {
// await can be used here
})();
The logic in the side effect function uses fetch() to retrieve the
user's post feed from the server. If the request succeeds, then
the list of posts is set in the posts state variable through the
setPosts() setter function.
When the state variable changes, React will trigger a new render
of the component, and this time the loop section of the JSX will
be used.
Here is Posts after the blog post rendering code was refactored:
return (
<>
{posts === undefined ?
<Spinner animation="border" />
:
<>
{posts === null ?
<p>Could not retrieve blog posts.</p>
:
<>
{posts.length === 0 ?
<p>There are no blog posts.</p>
:
posts.map(post => <Post key={post.id} post=
{post} />)
}
</>
}
</>
}
</>
);
}
The loop in Posts is now a single line that references Post for each
blog post. Something to remember when refactoring loops is that
the required key attribute must always be in the source file that
has the loop. React will not see it if it is moved into the child
component.
The new render code isn't very long, but there are a lot of
changes from the previous version.
The first child in the stack is the Image component from React-
Bootstrap, which is used to render the user's avatar image URL
that is returned by the server. The server works with Gravatar
URLs, which accept a s parameter in the query string to request
a specific image size, set to 48 pixels in this instance. The
roundCircle attribute makes the images round instead of square,
which gives the avatar a nice touch. Microblog API returns
Gravatar images that generate a geometric design for any email
addresses that do not have a registered avatar. You can visit
Gravatar to register an avatar for your email address.
The body of the post is rendered directly inside the second <p>.
The resulting page needed a few minor CSS adjustments to look
its best. Below are the CSS definitions added to index.css.
.Content {
margin-top: 10px;
}
.Post {
align-items: start;
padding-top: 5px;
border-bottom: 1px solid #eee;
}
.Post:hover {
background-color: #f8f8f8;
}
.Post a {
color: #14c;
text-decoration: none;
}
.Post a:visited {
color: #14c;
}
const secondsTable = [
['year', 60 * 60 * 24 * 365],
['month', 60 * 60 * 24 * 30],
['week', 60 * 60 * 24 * 7],
['day', 60 * 60 * 24],
['hour', 60 * 60],
['minute', 60],
];
const rtf = new Intl.RelativeTimeFormat(undefined, {numeric:
'auto'});
function getTimeAgo(date) {
// TODO
}
useEffect(() => {
const timerId = setInterval(
() => setUpdate(update => update + 1),
interval * 1000
);
return () => clearInterval(timerId);
}, [interval]);
return (
<span title={date.toString()}>{rtf.format(time, unit)}
</span>
);
}
The <span> element also has a title attribute, which renders the
date with its default string representation. Browsers create a
tooltip with the content of this attribute, which can be viewed by
hovering the mouse over the text of the element.
You now know how the relative times can be rendered, but in
addition to rendering them it would be nice if they automatically
updated as time passes. The state variable and side effect
function take care of this.
Figure 17 shows how blog posts look after all the improvements.
Figure 17 Blog posts rendered with improved styling
Chapter Summary
Use environment variables that start with the REACT_APP_
prefix in a .env file to provide configuration values such as
the base URL of the back end.
Render functions need to run and return quickly to prevent
the application from becoming unresponsive.
Use state variables to store data that needs to be retrieved
asynchronously, or that can change throughout the life of the
component.
Use side effect functions to perform network or other
asynchronous operations that update state variables. React
will automatically re-render a component when any of its
state variables change.
For state variables associated with data loaded from the
network, it is often useful to define specific values that
indicate that the data is being retrieved, and also that the
data retrieval has failed.
A dummy write-only state variable can be used to force a
component to re-render when none of its inputs have
changed.
A side effect function can return a cleanup function that
React calls as needed to prevent memory and resource
leaks.
OceanofPDF.com
Building an API Client
In Chapter 5, the Posts component was modified to make an API
call that gets data from the server. The way it was done,
however, does not scale very well, because as the application
continues to grow, there's going to be other components that will
also need to make their API calls, and having to repeat the API
calling logic in several places will be difficult to manage,
especially considering that these calls are going to become more
complex once authentication and pagination are implemented.
In this chapter you are going to learn how to provide API access
to any component that needs it, without having to duplicate
code. As part of this effort, you will learn how to use React
contexts and how to implement custom hook functions.
Consider the following way in which a client class for this API
might be used:
async request(options) {
let query = new URLSearchParams(options.query ||
{}).toString();
if (query !== '') {
query = '?' + query;
}
let response;
try {
response = await fetch(this.base_url + options.url +
query, {
method: options.method,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
body: options.body ? JSON.stringify(options.body) :
null,
});
}
catch (error) {
response = {
ok: false,
status: 500,
json: async () => { return {
code: 500,
message: 'The server is unresponsive',
description: error.toString(),
}; }
};
}
return {
ok: response.ok,
status: response.status,
body: response.status !== 204 ? await response.json() :
null
};
}
The class constructor stores the common part of the URL that is
used for all endpoints in the base_url attribute. This includes the
base URL obtained from the REACT_APP_BASE_API_URL environment
variable, and the /api path prefix.
Let's review the request() method in detail. This method takes all
of its arguments from an options object. The method and url keys
in this object are set by the get(), post(), put() and delete()
helper methods, from its input arguments. The body key is set by
post() and put(). Any additional options that the caller might
need, such as custom headers or query string parameters, are
accepted as a last argument on the four helper methods, and
passed through.
The request() method starts by looking for the query key in the
options, which allows the caller to specify query string
arguments as an object, for convenience and readability. The
URLSearchParams class available in the browser is used to render
the object in the proper query string format.
The ok, status and body keys returned in the fetch() response are
used to generate a simpler response object for the caller that
has the JSON payload already decoded to an object. Responses
from this API client will have three attributes:
Here the MyApp component inserts a context into its JSX tree. This
is done by using the Provider attribute of the context object that
was created with the createContext() function.
The custom hook can be added in the same source file as the
context object. Child components of the context then only need
to import the hook function to access the context, leading to
code that is more readable.
import { useMyData } from './MyDataContext';
return (
<ApiContext.Provider value={api}>
{children}
</ApiContext.Provider>
);
}
First, notice how this source file has two exported functions. The
ApiProvider component function is exported as the default symbol
for the module, as with all other React components, but now
there is also the custom hook useApi(), which has to be exported
so that other components can use it.
Now the Posts component can take advantage of the new hook to
make its API call.
useEffect(() => {
(async () => {
const response = await api.get('/feed');
if (response.ok) {
setPosts(response.body.data);
}
else {
setPosts(null);
}
})();
}, [api]);
The React build process analyzes your code, and one of the
things it looks for is possible dependency omissions. If you forget
to include api in the dependency array of the side effect function,
the build generates a warning to alert you that this variable is
used inside the function, which is a strong indicator that it should
be treated as a dependency.
useEffect(() => {
(async () => {
const response = await api.get('/users/' + username);
setUser(response.ok ? response.body : null);
})();
}, [username, api]);
return (
<Body sidebar>
{user === undefined ?
<Spinner animation="border" />
:
<>
{user === null ?
<p>User not found.</p>
:
<Stack direction="horizontal" gap={4}>
<Image src={user.avatar_url + '&s=128'}
roundedCircle />
<div>
<h1>{user.username}</h1>
{user.about_me && <h5>{user.about_me}</h5>}
<p>
Member since: <TimeAgo isoDate=
{user.first_seen} />
<br />
Last seen: <TimeAgo isoDate={user.last_seen}
/>
</p>
</div>
</Stack>
}
</>
}
</Body>
);
}
The side effect function makes the API request, and calls
setUser() to update the state variable with the response. As in
the Posts component, the state variable starts with an undefined
value, and once the request returns it is updated to the user
information when the request succeeds, or to null if the request
fails.
If you leave the dependency array for the side effect function
empty, the React build warns that both api and username are
dependencies, so both should be included, so that the
component updates when they change. Making the username
variable a dependency is extremely important, as it would cause
the user information to be refreshed whenever the user changes.
The render code uses the Body component and includes the
sidebar. Inside the body, if the user state variable is undefined a
spinner is rendered to indicate that the data is being retrieved.
With these changes, you can click on any username in the feed
page to access their profile page. This is shown in Figure 18.
Figure 18 User profile page
let url;
switch (content) {
case 'feed':
case undefined:
url = '/feed';
break;
case 'explore':
url = '/posts';
break
default:
url = `/users/${content}/posts`;
break;
}
useEffect(() => {
(async () => {
const response = await api.get(url);
if (response.ok) {
setPosts(response.body.data);
}
else {
setPosts(null);
}
})();
}, [api, url]);
In the previous version of this component, the URL for the API
request was hardcoded to /feed. Now a switch statement is used
to determine what URL to request based on the value of the
content prop:
All these URLs return lists of blog posts, and they all use the
same response format. But each URL returns a different list.
Remember that you can review the documentation for the
endpoints in the documentation site provided by Microblog API.
The user profile page now can include a list of posts by the user
being viewed. The changes to UserPage are below.
This change just wraps the Stack component in <> and </>, so that
a Posts component can be added after it, while keeping a single
parent in the JSX tree. The Posts component in this case has the
content prop set to user.id, to request that only the posts from
the user in question are rendered.
Pagination
The lists of blog posts returned by Microblog API are paginated,
which means that for a given request, the server returns the first
few items, up to a configurable maximum (25 with default
settings).
{
"data": [
],
"pagination": {
"count": 25,
"limit": 25,
"offset": 0,
"total": 124
}
}
To let the user add for more elements, a "More" button is going
to be displayed at the bottom of the list, when there are more
elements available. Clicking the button will trigger a new request
to be issued for more items.
return (
<div className="More">
{thereAreMore &&
<Button variant="outline-primary" onClick=
{loadNextPage}>
More »
</Button>
}
</div>
);
}
The More component has two props. The pagination prop receives
a pagination object extracted from a Microblog API response,
including the four attributes discussed above. The loadNextPage
prop is a handler that the parent component must provide, used
to load the next page of items when the More button is clicked.
Requiring the parent component to provide the logic to load the
next page allows this component to be fully generic, instead of
being tied to a specific part of the application.
.More {
margin-top: 10px;
margin-bottom: 10px;
text-align: right;
}
useEffect(() => {
(async () => {
const response = await api.get(url);
if (response.ok) {
setPosts(response.body.data);
setPagination(response.body.pagination);
}
else {
setPosts(null);
}
})();
}, [api, url]);
return (
<>
{posts === undefined ?
<Spinner animation="border" />
:
<>
{posts === null ?
<p>Could not retrieve blog posts.</p>
:
<>
{posts.length === 0 ?
<p>There are no blog posts.</p>
:
posts.map(post => <Post key={post.id} post=
{post} />)
}
<More pagination={pagination} loadNextPage=
{loadNextPage} />
</>
}
</>
}
</>
);
}
With these changes, you should see the More button after the
blog posts. If you don't see it, then the list of posts that you are
looking at does not have additional data. With the initial
randomized data that is initialized with Microblog API, the
Explore page should have enough posts to require pagination,
but the Feed page may not. Figure 20 shows how the pagination
button looks like.
The limit option is used to change the size of the returned page.
This application does not need to do this, the client is happy to
accept the default page size used by the server, so this option
does not need to be used in this pagination implementation.
The function makes a request on the same url as before, but this
time the second argument for the api.get() call is added with the
after query parameter.
After the new posts are displayed, the More component will
remain at the bottom, so the updated pagination state will be
used again to determine if there are even more items waiting in
the server, and in that case the button will continue to be visible.
After the last batch of items are retrieved, the More component
will not display the button anymore.
Chapter Summary
Write the back end calling logic in a single place, for example
in an API client class.
To share a data item with a subcomponent, pass it down with
a prop. To share a data item with many subcomponents
across several levels, use a context.
Use a custom hook function to make the code that uses a
context more readable.
Use props to make a component more generic, and facilitate
its reuse.
OceanofPDF.com
Forms and Validation
A big part of most web applications is accepting, validating and
processing user input. In this chapter you are going to learn how
to perform these tasks with React by creating the user
registration and login forms for Microblog.
<Form>
<Form.Group className="mb-3" controlId="formBasicEmail">
<Form.Label>Email address</Form.Label>
<Form.Control type="email" placeholder="Enter email" />
<Form.Text className="text-muted">
{"We'll never share your email with anyone else."}
</Form.Text>
</Form.Group>
</Form>
The label is omitted if the label prop was not passed by the
parent component
The type prop is optional, and defaults to text when not given
by the parent
The error message uses a text-danger style from Bootstrap,
which renders the text in red
The placeholder and error props are optional, and will render
as empty text when not provided by the parent
return (
<Body>
<h1>Login</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username or email address"
error={formErrors.username} />
<InputField
name="password" label="Password" type="password"
error={formErrors.password} />
<Button variant="primary" type="submit">Login</Button>
</Form>
</Body>
);
}
The page uses the Body component as its main wrapper, as all
other pages. The login page will not offer navigation links, so for
that reason the sidebar prop is not included.
The Form component has a onSubmit prop, which configures a
handler function that will be invoked when the form is submitted.
The handler starts by doing something very important: it
disables the browser's own form submission logic, by calling the
ev.preventDefault() method of the event object that was passed
as an argument. This is necessary to prevent the browser from
sending a network request with the form data.
The form implements a standard two field login form, using the
InputField component defined above. The name, label and error
props are passed for both fields. For the first field type isn't
passed, so the default type of text is used. The password field
needs to have the type explicitly set, so that the characters are
not visible. The placeholder prop is not used in any of the fields in
this form.
return (
<form>
<input type="text" ref={usernameField} />
</form>
);
}
return (
<form onSubmit={onSubmit}>
<input type="text" ref={usernameField} />
</form>
);
}
useEffect(() => {
usernameField.current.focus();
}, []);
return (
<Body>
<h1>Login</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username or email address"
error={formErrors.username} fieldRef={usernameField}
/>
<InputField
name="password" label="Password" type="password"
error={formErrors.password} fieldRef={passwordField}
/>
<Button variant="primary" type="submit">Login</Button>
</Form>
</Body>
);
}
The onSubmit event handler for the form retrieves the values of
the referenced input fields in the DOM, and for now, logs them to
the console so that you can verify that everything is working
when the form is submitted.
useEffect(() => {
usernameField.current.focus();
}, []);
return (
<Body>
<h1>Register</h1>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username"
error={formErrors.username} fieldRef={usernameField}
/>
<InputField
name="email" label="Email address"
error={formErrors.email} fieldRef={emailField} />
<InputField
name="password" label="Password" type="password"
error={formErrors.password} fieldRef={passwordField}
/>
<InputField
name="password2" label="Password again"
type="password"
error={formErrors.password2} fieldRef={password2Field}
/>
<Button variant="primary"
type="submit">Register</Button>
</Form>
</Body>
);
}
return (
<Body>
<h1>Login</h1>
... // <-- no changes to the form
<hr />
<p>Don't have an account? <Link
to="/register">Register here</Link>!</p>
</Body>
);
}
If the request fails due to validation, the Microblog API back end
returns an error response. The body of the response includes
detailed information about the error, and in particular, the
errors.json object contains validation error messages for each
field. This is actually very convenient, because the formErrors
state variable uses the same format. To display the validation
errors from the server, this object is set directly on the state
variable. Figure 24 shows an example of server-side validation
errors displayed on the form.
return ( ... );
}
}
This is a really nice pattern, because any component that needs
to flash a message can just get the flash() function from the
useFlash() hook, without having to worry about how or where the
alert is going to render in the page.
<FlashProvider>
<Alert />
<MyForm />
</FlashProvider>
return (
<FlashContext.Provider value={{flash, hideFlash,
flashMessage, visible}}>
{children}
</FlashContext.Provider>
);
}
return (
<Collapse in={visible}>
<div>
<Alert variant={flashMessage.type || 'info'} dismissible
onClose={hideFlash}>
{flashMessage.message}
</Alert>
</div>
</Collapse>
);
}
OceanofPDF.com
Authentication
Up to this point, you have been using the Microblog API back
end with an option to bypass authentication. This enabled
the project to grow without having to deal with the highly
complex matter of user authentication up front. In this
chapter you will finally learn how to do this.
You may be wondering why this code isn't checking that the
access token is not null before including it in the request.
The assumption is that if the client object doesn't have an
access token, then a different Authorization header will be
included in the options object passed by the caller, which
overrides the Authorization header defined here.
There are three possible outcomes for this function. The two
most obvious ones indicate authentication success or
failure. A third less likely case occurs when the
authentication request fails due to an unexpected issue, and
not because the credentials are invalid. The return value of
this function can be ok, fail or error to represent these three
cases. The error case occurs when the authentication
request returns a failure status code other than 401.
useEffect(() => {
(async () => {
if (api.isAuthenticated()) {
const response = await api.get('/me');
setUser(response.ok ? response.body : null);
}
else {
setUser(null);
}
})();
}, [api]);
return (
<UserContext.Provider value={{ user, setUser, login,
logout }}>
{children}
</UserContext.Provider>
);
}
If the api instance does not have an access token, then the
user state variable is set to null to indicate to the rest of the
application that there is no user logged in.
The logout() helper function logs the user out in the api
client instance, and then updates the user state variable to
null, so that all components that use the context know that
there is no authenticated user anymore.
The UserContext.Provider component sets the value of the
context as an object with the user, setUser, login and logout
keys.
Public Routes
The login and user registration routes have the interesting
property that they have no purpose after the user logs in. To
prevent a logged-in user from navigating to these routes, it
is a good idea to also create a PublicRoute component that
only allows its children to render when the user is not
logged in, or else it redirects the user to the root URL of the
application.
<Routes>
<Route path="/login" element={
<PublicRoute><LoginPage /></PublicRoute>
} />
<Route path="/register" element={
<PublicRoute><RegistrationPage /></PublicRoute>
} />
<Route path="*" element={
<PrivateRoute>
<Routes>
<Route path="/" element={<FeedPage />} />
<Route path="/explore" element={<ExplorePage
/>} />
<Route path="/user/:username" element=
{<UserPage />} />
<Route path="*" element={<Navigate to="/" />}
/>
</Routes>
</PrivateRoute>
} />
</Routes>
The new logic to log the user in is added at the end of the
onSubmit() function, after all the validation checks have
passed. Note that this function was converted to async, so
that await can be used.
return (
<Navbar bg="light" sticky="top" className="Header">
<Container>
<Navbar.Brand>Microblog</Navbar.Brand>
<Nav>
{user === undefined ?
<Spinner animation="border" />
:
<>
{user !== null &&
<div className="justify-content-end">
<NavDropdown title={
<Image src={user.avatar_url + '&s=32'}
roundedCircle />
} align="end">
<NavDropdown.Item as={NavLink} to=
{'/user/' + user.username}>
Profile
</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item onClick={logout}>
Logout
</NavDropdown.Item>
</NavDropdown>
</div>
}
</>
}
</Nav>
</Container>
</Navbar>
);
}
When the access token expires, the client can request a new
access token by sending a PUT request to /api/tokens,
passing the expired access token in the body of the request.
In addition to the expired access token, the client must
provide a refresh token through a cookie that the server set
during the initial authentication flow. The cookie that stores
the refresh token is secured so that it cannot be stolen via
JavaScript based attacks.
async requestInternal(options) {
... // <-- original request() code here
}
Once the client is updated with the new access token, the
original request can be retried, and this time the response is
directly returned, regardless of its status code.
OceanofPDF.com
Application Features
By now you have learned most of the React concepts you need
to complete this application. This chapter is dedicated to building
the remaining features of the application, with the goal of
solidifying the knowledge you acquired in previous chapters.
useEffect(() => {
textField.current.focus();
}, []);
return (
<Stack direction="horizontal" gap={3} className="Write">
<Image
src={ user.avatar_url + '&s=64' }
roundedCircle
/>
<Form onSubmit={onSubmit}>
<InputField
name="text" placeholder="What's on your mind?"
error={formErrors.text} fieldRef={textField} />
</Form>
</Stack>
);
}
The only aspect of this form that is different from previous ones
is the showPost prop. When a user writes a blog post, the
application needs to immediately add the post to the feed at the
top position. Since this component does not know anything
about modifying the displayed feed, it accepts a callback
function provided by the parent component to perform this
action. As far as the Write component is concerned, all that
needs to be done after successfully creating a blog post is to call
the function passed in showPost with the new post object (which
Microblog API returns in the body of the response) as an
argument.
.Write {
margin-bottom: 10px;
padding: 30px 0px 40px 0px;
border-bottom: 1px solid #eee;
}
.Write form {
width: 100%;
}
return (
<>
{write && <Write showPost={showPost} />}
... // <-- no changes to existing JSX
</>
);
The component adds a write prop. If this prop has a truthy value,
then the JSX for the Write component is rendered above the post
list.
You should now be able to enter blog posts into the system from
the feed page. Figure 27 shows how this page looks after a first
post was entered into the system.
The listing that follows shows the changes that need to be made
to the UserPage component to display a button to trigger the
correct action out of the three possibilities discussed above.
useEffect(() => {
(async () => {
const response = await api.get('/users/' + username);
if (response.ok) {
setUser(response.body);
if (response.body.username !== loggedInUser.username) {
const follower = await api.get(
'/me/following/' + response.body.id);
if (follower.status === 204) {
setIsFollower(true);
}
else if (follower.status === 404) {
setIsFollower(false);
}
}
else {
setIsFollower(null);
}
}
else {
setUser(null);
}
})();
}, [username, api, loggedInUser]);
return (
<Body sidebar>
{user === undefined ?
<Spinner animation="border" />
:
<>
{user === null ?
<p>User not found.</p>
:
<>
<Stack direction="horizontal" gap={4}>
<Image src={user.avatar_url + '&s=128'}
roundedCircle />
<div>
... // <-- no changes to user details
With these changes, the page has a new state variable isFollower
that will be true if the logged-in user is following the user being
viewed, or false if it isn't. A value of null will be used to indicate
that the logged-in user and the viewed user are the same, and
as always, undefined is the initial value of the state variable,
indicating that the side effect function hasn't determined which
of the three possible states is the correct state to use.
The side effect function obtains the viewed user and stores it in
the user state variable, and then needs to determine the value of
the isFollower state variable, which determines which of the
three possible operations apply. If the viewed user is different
from the logged-in user, a second request is sent to the back end
to obtain the follow relationship between the two users, which is
used to update the isFollower state variable.
The buttons that present the actions to the user are going to
have onClick handlers edit(), follow() and unfollow() respectively,
all placeholders for now.
When users view their own profile page, they'll see an "Edit"
button. The handler for this button in the UserPage component will
redirect to a new route that displays a form with user information
fields.
...
);
}
useEffect(() => {
usernameField.current.value = user.username;
emailField.current.value = user.email;
aboutMeField.current.value = user.about_me;
usernameField.current.focus();
}, [user]);
return (
<Body sidebar={true}>
<Form onSubmit={onSubmit}>
<InputField
name="username" label="Username"
error={formErrors.username} fieldRef={usernameField}
/>
<InputField
name="email" label="Email"
error={formErrors.email} fieldRef={emailField} />
<InputField
name="aboutMe" label="About Me"
error={formErrors.about_me} fieldRef={aboutMeField} />
<Button variant="primary" type="submit">Save</Button>
</Form>
</Body>
);
}
This component renders a form with three fields for the user to
change the username, email address or "about me" information.
The fields are all instances of the InputField component, and are
provisioned with references as seen in previous forms.
Other forms had a side effect function that put the focus on the
first field of the form. In this component, the side effect function
does that as well, but it also pre-populates the three input fields
with their current values, obtained from the user object returned
by the useUser() hook. Assigning values to form fields is done
through the DOM, using the reference objects assigned to the
input fields.
The JSX code for this form renders the form with the three fields
and a submit button, as done in previous forms. Figure 28 shows
how the edit user form looks.
Figure 28 Edit user form
With these changes, you can go to the Explore page, click on one
or more usernames, and follow them. The blog posts from the
users you follow will appear on your feed page along with your
own posts.
return (
...
...
);
}
useEffect(() => {
oldPasswordField.current.focus();
}, []);
return (
<Body sidebar>
<h1>Change Your Password</h1>
<Form onSubmit={onSubmit}>
<InputField
name="oldPassword" label="Old Password"
type="password"
error={formErrors.old_password} fieldRef=
{oldPasswordField} />
<InputField
name="password" label="New Password" type="password"
error={formErrors.password} fieldRef={passwordField}
/>
<InputField
name="password2" label="New Password Again"
type="password"
error={formErrors.password2} fieldRef={password2Field}
/>
<Button variant="primary" type="submit">Change
Password</Button>
</Form>
</Body>
);
}
Password Resets
The last feature of the application that will be added in this
chapter is the option for users to request a password reset when
they forget their account password. This feature is implemented
in two parts:
First, the user must click a "Forgot Password" link in the login
page to access the password reset request page. On this
page, the user must enter their email address. If the email
address is valid, the server will send an email with a
password reset link.
The second part of the reset process occurs when the user
clicks the password reset link received by email. The link
contains a token that needs to be submitted to the server for
verification, along with a new password. If the token is valid,
then the password is updated.
return (
...
...
);
}
The login page needs a link to a new page in which the user can
request a password reset.
Listing 92 src/pages/LoginPage.js: Reset password link
// add this above the registration link
<p>Forgot your password? You can <Link to="/reset-request">reset
it</Link>.</p>
useEffect(() => {
emailField.current.focus();
}, []);
return (
<Body>
<h1>Reset Your Password</h1>
<Form onSubmit={onSubmit}>
<InputField
name="email" label="Email Address"
error={formErrors.email} fieldRef={emailField} />
<Button variant="primary" type="submit">Reset
Password</Button>
</Form>
</Body>
);
}
The reset request form has a single field, in which the user must
enter the email address for the account to reset. The email is
submitted in a POST request to the /api/tokens/reset URL, and as a
result of this request the user should receive an email with a
reset link.
The email reset links that Microblog API sends out to users have
a query string parameter called token. The value of this token,
along with the new chosen password for the account must be
sent on a PUT request to the /api/tokens/reset endpoint. If the
server determines that the reset token is valid, then it updates
the password on the account and returns a response with a 200
status code.
useEffect(() => {
if (!token) {
navigate('/');
}
else {
passwordField.current.focus();
}
}, [token, navigate]);
const onSubmit = async (event) => {
event.preventDefault();
if (passwordField.current.value !==
password2Field.current.value) {
setFormErrors({password2: "New passwords don't match"});
}
else {
const response = await api.put('/tokens/reset', {
token,
new_password: passwordField.current.value
});
if (response.ok) {
setFormErrors({});
flash('Your password has been reset.', 'success');
navigate('/login');
}
else {
if (response.body.errors.json.new_password) {
setFormErrors(response.body.errors.json);
}
else {
flash('Password could not be reset. Please try
again.', 'danger');
navigate('/reset-request');
}
}
}
};
return (
<Body>
<h1>Reset Your Password</h1>
<Form onSubmit={onSubmit}>
<InputField
name="password" label="New Password" type="password"
error={formErrors.password} fieldRef={passwordField}
/>
<InputField
name="password2" label="New Password Again"
type="password"
error={formErrors.password2} fieldRef={password2Field}
/>
<Button variant="primary" type="submit">Reset
Password</Button>
</Form>
</Body>
);
}
The form implemented in this component has two fields for the
user to enter the new password twice. Client-side validation
ensures that these two fields have the same value.
To test the password reset flow, first make sure you are not
logged in. From the login page, click on the reset link and enter
your email. Click on the link that you receive a few seconds later
and enter a new password for your account. If you do not receive
an email from Microblog API, then your email server settings are
probably incorrect, so go back to Chapter 5 to review them.
Chapter Summary
A convenient pattern to create reusable child components is
for parents to create callback functions with customized
behaviors and pass them to the child component as props.
If you don't like some aspects of how a third-party
component works, you may be able to create a wrapper
component that overrides and improves the original
component.
You can pre-populate form fields in a side effect function
using DOM APIs and field references.
To prevent XSS attacks, React escapes all rendered variables.
If you need to store HTML in a variable intended for
rendering, use a JSX expression instead of a plain string.
Application routes are proper links that can be bookmarked
or even shared in emails or other communication channels.
OceanofPDF.com
Memoization
An important part of React that you haven't seen yet is
memoization, a technique that helps prevent unnecessary
renders and improve the overall performance of the
application. In this chapter you are going to learn how the
React rendering algorithm works, and how you can make it
work efficiently for your application.
Once the two phases of the first render complete and the
application is visible on the page, React runs the side effect
functions that were started by many of the components.
These functions do some work, and will eventually update
some of the state variables that are held by components.
But at this point the user may click a button, or a link. Event
handlers attached to these UI elements will be invoked by
the browser, and these will very likely end up changing
some state variables, starting another chain of renders.
Render Loops
Another common problem related to the React rendering
algorithm happens when endless render cycles are
inadvertently introduced across many components. These
occur when application re-renders parts of its tree
indefinitely due to cyclical dependencies. An endless render
cycle will cause high CPU usage and poor application
performance, so it should be avoided. The application in its
current state does not have any render loops, but it is
actually easy to inadvertently introduce one.
async request(options) {
let response = await this.requestInternal(options);
if (response.status === 401 && options.url !==
'/tokens') {
... // <-- no changes to retry logic
}
if (response.status >= 500 && this.onError) {
this.onError(response);
}
return response;
}
How can you prevent loops such as this one? You can study
the source code for all the components involved, and you
may still not see a straightforward way to break the loop.
These components just have a circular chain of
relationships, so the first solution you may consider is to
eliminate some dependencies, at the cost of removing
features. For example, if the onError() handler didn't use the
flash() function, then this issue wouldn't case a render loop.
OceanofPDF.com
Automated Testing
Up to now, all the testing done on the application you've
built was manual. Manual testing as you develop your
application is useful, but as the application grows the
required testing effort grows as well, until it becomes so
time-consuming that the only way to keep up is to test less,
or to find a way to automate some testing work.
To start the test suite, you can run the following command
from a terminal:
npm test
Jest may also decide that none of the changes you made
recently to the project affect App.js, and in that case it will
not run any tests and just start to watch for changes with
the following message:
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
If you get this output, you press "a" to instruct Jest to run
the test suite. I recommend that you do this, so that you
become familiar with how Jest shows test failures.
expect(element).toBeInTheDocument();
expect(element).toHaveClass('navbar-brand');
});
The new version of the test still renders App, but then it runs
a query to locate an element in the page with the text
"Microblog", which should return the title element in the
heading.
In this test there are two assertions. First the test ensures
that the element was found in the page. As an additional
check, it makes sure that the element has the class navbar-
brand, which comes from Bootstrap.
Using Queries
For each of these six query options, you can choose from a
selection of query options. Above you've seen how to locate
an element by its text with getByText(). Other options are
getByTitle(), getByAltText(), getByRole() and more that you
can see in the documentation.
Using Assertions
expect(result).toBeNull();
render(
<BrowserRouter>
<Post post={post} />
</BrowserRouter>
);
expect(message).toBeInTheDocument();
expect(authorLink).toBeInTheDocument();
expect(authorLink).toHaveAttribute('href',
'/user/susan');
expect(avatar).toBeInTheDocument();
expect(avatar).toHaveAttribute('src',
'https://example.com/avatar/susan&s=48');
expect(timestamp).toBeInTheDocument();
expect(timestamp).toHaveAttribute(
'title', 'Sat Jan 01 2022 00:00:00 GMT+0000 (Greenwich
Mean Time)');
});
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers()
});
render(
<FlashProvider>
<FlashMessage />
<Test />
</FlashProvider>
);
expect(alert).toHaveTextContent('foo');
expect(alert).toHaveClass('alert-danger');
});
The test above ensures that alerts are displayed, but the
flash message system in this application has some
complexity that the test isn't reaching. In particular, alerts
are supposed to close on their own after 10 seconds. How
can that be tested, ideally without having to wait those 10
seconds?
return (
<Collapse in={visible}>
<div>
<Alert variant={flashMessage.type || 'info'}
dismissible
onClose={hideFlash} data-visible={visible}>
{flashMessage.message}
</Alert>
</div>
</Collapse>
);
}
expect(alert).toHaveTextContent('foo');
expect(alert).toHaveClass('alert-danger');
expect(alert).toHaveAttribute('data-visible', 'true');
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
global.fetch = realFetch;
localStorage.clear();
});
The next listing shows the first test, which logs a user in.
This code goes right after the afterEach() handler.
global.fetch
.mockImplementationOnce(url => {
urls.push(url);
return {
status: 200,
ok: true,
json: () => Promise.resolve({access_token: '123'}),
};
})
.mockImplementationOnce(url => {
urls.push(url);
return {
status: 200,
ok: true,
json: () => Promise.resolve({username: 'susan'}),
};
});
render(
<FlashProvider>
<ApiProvider>
<UserProvider>
<Test />
</UserProvider>
</ApiProvider>
</FlashProvider>
);
The new and most interesting part of this test is at the start
of the function. The urls array is going to be used to collect
all the URLs that the application calls fetch() on. These calls
are now going to be redirected to the mock function, so the
test has full control of what happens in these calls.
The first API call that the application makes goes to the
/api/tokens endpoint to request an access token. The first
fake function registered with the mock returns a success
response with status code 200 and a made up 123 token.
global.fetch
.mockImplementationOnce(url => {
urls.push(url);
return {
status: 401,
ok: false,
json: () => Promise.resolve({}),
};
});
render(
<FlashProvider>
<ApiProvider>
<UserProvider>
<Test />
</UserProvider>
</ApiProvider>
</FlashProvider>
);
Testing Logouts
The last authentication test ensures that users can log out
of the application.
localStorage.setItem('accessToken', '123');
render(
<FlashProvider>
<ApiProvider>
<UserProvider>
<Test />
</UserProvider>
</ApiProvider>
</FlashProvider>
);
userEvent.click(button);
const element2 = await screen.findByText('logged out');
expect(element2).toBeInTheDocument();
expect(localStorage.getItem('accessToken')).toBeNull();
});
The mocked fetch() function for this test includes two calls.
The first mocks the response to the /api/me request issued
by the UserProvider component. A second mocked is
response is included for the token revocation request issued
during logout.
OceanofPDF.com
Production Builds
You have an application that you have been using in your
own computer during development. How do you put this
application in front of your users? In this chapter you are
going to learn how to work with production builds of your
application.
https://cra.link/deployment
Note that for this command to work, you have to stop the
development web server, which also runs on port 3000.
Structure of a Production Built Application
For each .js and .css file, React also includes corresponding
.js.map and .css.map. Map files make it possible to perform
some debugging tasks on the production build. As a side
effect of that they may also allow your users to inspect a
fairly readable version of your application's source code, so
depending on your particular case you may decide not to
copy the map files to the production server.
Netlify
Cloudflare
GitHub Pages
Digital Ocean's App Platform
Heroku
AWS
and many more.
{
...
"scripts": {
...
"deploy": "npm run build && [your deploy command here]"
},
...
}
Reverse Proxy
server {
listen 80;
server_name localhost;
root /home/ubuntu/react-microblog/build;
index index.html;
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
location /static {
expires 1y;
add_header Cache-Control "public";
}
location /api {
proxy_pass http://localhost:5000;
}
}
When you are done testing, you can stop and delete the
container with this command:
docker rm -f microblog
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
location /static {
expires 1y;
add_header Cache-Control "public";
}
location /api {
proxy_pass http://api:5000;
}
}
This version of the configuration uses the most appropriate
settings for the production build files of the React
application. An /api location is also added to proxy requests
to a back end server, which is assumed to be running on a
host named api, on port 5000. In the next section, you'll add
a second container for the back end, and this container will
be associated with the api hostname.
What happens next? This is pretty much it. If you follow the
deployment steps on your production host, when you reach
this point you have a fully working application. While this is
outside the scope of this article, the Nginx configuration can
be further expanded to use a proper domain name that is
associated with an SSL certificate.
$ docker-compose down
[+] Running 3/3
- Container react-microblog-api-1 Removed
- Container react-microblog-frontend-1 Removed
- Network react-microblog_default Removed
Next Steps
Congratulations on completing this course! In this short
section I want to give you some pointers on how to continue
on your learning path.
If Python and/or Flask are not your thing, you should be able
to find resources on writing APIs and services in your
favorite programming language and web framework.
OceanofPDF.com
Index
A|B|C|D|E|G|H|I|J|M|N|P|R|S|T
A
Access tokens, [1] Automated testing
B
Basic authentication, [1] Bearer authentication, [1]
bootstrap package, [1]
C
Create React App Cross-site scripting
attacks (XSS), [1]
D
Docker, [1] Docker Compose
E
ECMA ECMAScript
Email server
G
Gravatar service
H
Heroku
I
ISO 8601
J
JavaScript Jest
Arrow functions Mocking
Async/await, [1]
Classes, [1]
Constants
Destructuring assignments,
[1], [2]
Equality comparisons
ES5
ES6
Exports
fetch function, [1]
for ... of statement
Imports
Inequality comparisons
Intl.RelativeTimeFormat
class
JavaScript XML (JSX)
JSX
Object property shorthand,
[1]
Promises
Semicolons
setInterval function
setTimeout function
Spread operator, [1], [2]
String interpolation, [1]
Trailing commas
Variables
M
Microblog API back end, [1]
Authentication
Docker deployment, [1]
Documentation
Heroku deployment
Python deployment
N
Nginx, [1] npm start command
Node.js npx command
P
Python
R
React react-router-dom
App.js file package, [1], [2]
caching production files BrowserRouter
className attribute component
Controlled components Link component
createContext function Navigate
custom hooks component, [1]
favicon.ico file NavLink
fragments component, [1]
icon files Route component
index.html file Routes
index.js file component
key attribute useLocation hook
manifest.json file useNavigate hook
memo function useParams hook
Production builds Refresh tokens, [1]
props
ref attribute, [1]
rendering conditionals
rendering lists
rendering variables
Uncontrolled
components
useCallback hook
useContext hook, [1]
useEffect hook, [1]
useMemo hook
useRef hook
useState hook, [1]
Virtual DOM
React Testing Library
react-bootstrap package, [1]
Alert component, [1]
Button component
Collapse component
Container component,
[1], [2]
Form component
Image component, [1]
Link component
Nav component, [1]
Navbar component, [1]
NavDropdown
component
Stack component, [1],
[2], [3], [4]
react-microblog application
.env file
ApiProvider component,
[1], [2]
App component, [1], [2],
[3], [4], [5], [6], [7], [8],
[9], [10], [11], [12], [13],
[14], [15]
App tests
Body component, [1]
ChangePasswordPage
component
EditUserPage component
ExplorePage component,
[1]
FeedPage component,
[1]
FlashMessage
component
FlashProvider
component, [1]
FlashProvider tests
Header component, [1],
[2]
index.css file, [1], [2],
[3], [4]
index.html file
index.js file
InputField component,
[1], [2]
LoginPage component,
[1], [2], [3], [4], [5], [6]
manifest.json file
MicroblogApiClient class,
[1], [2], [3]
More component
package.json file
Post component, [1]
Post tests
Posts component, [1],
[2], [3], [4], [5], [6], [7]
PrivateRoute component
PublicRoute component
RegistrationPage
component, [1]
ResetPage component
ResetRequestPage
component
Sidebar component, [1]
TimeAgo component, [1]
useApi hook, [1]
useFlash hook
UserPage component,
[1], [2], [3]
UserProvider component,
[1]
UserProvider tests
useUser hook, [1], [2]
Write component
S
SendGrid email service single-page application
serve package, [1] (SPA)
SPA
T
transpiling
OceanofPDF.com
OceanofPDF.com
OceanofPDF.com
Semicolons
const a = 1; // <-- semicolon here
function f() {
console.log('this is f'); // <-- semicolon here
} // <-- but not here
const f = () => {
console.log('this is f');
}; // <-- this is an assignment, so a semicolon is used
Trailing Commas
const myArray = [
1,
3,
5,
];
const myObject = {
name: 'susan',
age: 20,
};
let a = 1;
const c = 3;
console.log(c); // 3
c = 4; // error
For-Of Loops
const allTheNames = ['susan', 'john', 'alice'];
for (name of allTheNames) {
console.log(name);
}
Arrow Functions
function mult(x, y) {
const result = x * y;
return result;
}
mult(2, 3); // 6
mult(2, 3); // 6
square(2); // 4
Promises
fetch('https://example.com').then(r => console.log(r.status));
fetch('http://example.com/data.json')
.then(r => r.json())
.then(data => console.log(data));
fetch('http://example.com/data.json')
.then(r => r.json())
.then(data => console.log(data))
.catch(error => console.log(`Error: ${error}`));
Async and Await
fetch('http://example.com/data.json')
.then(r => r.json())
.then(data => console.log(data));
Spread Operator
const a = [5, 3, 9, 2, 7];
console.log(Math.min(...a)); // 2
const myTable = (
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Susan</td>
<td>20</td>
</tr>
<tr>
<td>John</td>
<td>45</td>
</tr>
</table>
);
OceanofPDF.com