Learning React Modern Patterns For Developing React Apps 2nbsped Compress
Learning React Modern Patterns For Developing React Apps 2nbsped Compress
Preface
a. Conventions Used in This Book
b. Using Code Examples
c. O’Reilly Online Learning
d. How to Contact Us
e. Acknowledgments
2. 1. Welcome to React
a. A Strong Foundation
b. React’s Past and Future
i. Learning React: Second Edition Changes
c. Working with the Files
i. File Repository
ii. React Developer Tools
iii. Installing Node.js
f. Classes
g. ES6 Modules
i. CommonJS
4. 3. Functional Programming with JavaScript
a. What It Means to Be Functional
b. Imperative Versus Declarative
c. Functional Concepts
i. Immutability
ii. Pure Functions
iii. Data Transformations
iv. Higher-Order Functions
v. Recursion
vi. Composition
vii. Putting It All Together
5. 4. How React Works
a. Page Setup
b. React Elements
c. ReactDOM
i. Children
d. React Components
i. React Components: A Historical Tour
6. 5. React with JSX
a. React Elements as JSX
i. JSX Tips
ii. Mapping Arrays with JSX
b. Babel
c. Recipes as JSX
d. React Fragments
e. Intro to webpack
i. Creating the Project
ii. Loading the Bundle
iii. Source Mapping
iv. Create React App
7. 6. React State Management
a. Building a Star Rating Component
b. The useState Hook
c. Refactoring for Advanced Reusability
d. State in Component Trees
i. Sending State Down a Component Tree
ii. Sending Interactions Back up a
Component Tree
e. Building Forms
i. Using Refs
ii. Controlled Components
iii. Creating Custom Hooks
iv. Adding Colors to State
f. React Context
i. Placing Colors in Context
ii. Retrieving Colors with useContext
iii. Stateful Context Providers
iv. Custom Hooks with Context
8. 7. Enhancing Components with Hooks
a. Introducing useEffect
i. The Dependency Array
ii. Deep Checking Dependencies
iii. When to useLayoutEffect
iv. Rules to Follow with Hooks
v. Improving Code with useReducer
vi. useReducer to Handle Complex State
vii. Improving Component Performance
viii. shouldComponentUpdate and
PureComponent
ix. When to Refactor
9. 8. Incorporating Data
a. Requesting Data
i. Sending Data with a Request
ii. Uploading Files with fetch
iii. Authorized Requests
iv. Saving Data Locally
v. Handling Promise States
b. Render Props
c. Virtualized Lists
i. Creating a Fetch Hook
ii. Creating a Fetch Component
iii. Handling Multiple Requests
iv. Memozing Values
v. Waterfall Requests
vi. Throttling the Network Speed
vii. Parallel Requests
viii. Waiting for Values
ix. Canceling Requests
d. Introducing GraphQL
i. GitHub GraphQL API
ii. Making a GraphQL Request
10. 9. Suspense
a. Error Boundaries
b. Code Splitting
i. Introducing: The Suspense Component
ii. Using Suspense with Data
iii. Throwing Promises
iv. Building Suspenseful Data Sources
v. Fiber
11. 10. React Testing
a. ESLint
i. ESLint Plug-Ins
b. Prettier
i. Configuring Prettier by Project
ii. Prettier in VSCode
c. Typechecking for React Applications
i. PropTypes
ii. Flow
iii. TypeScript
d. Test-Driven Development
i. TDD and Learning
e. Incorporating Jest
Copyright © 2020 Alex Banks and Eve Porcello. All rights reserved.
The views expressed in this work are those of the authors, and do not
represent the publisher’s views. While the publisher and the authors
have used good faith efforts to ensure that the information and
instructions contained in this work are accurate, the publisher and the
authors disclaim all responsibility for errors or omissions, including
without limitation responsibility for damages resulting from the use of
or reliance on this work. Use of the information and instructions
contained in this work is at your own risk. If any code samples or other
technology this work contains or describes is subject to open source
licenses or the intellectual property rights of others, it is your
responsibility to ensure that your use thereof complies with such
licenses and/or rights.
978-1-492-05172-5
[LSI]
Preface
This book is for developers who want to learn the React library while
learning the latest techniques currently emerging in the JavaScript
language. This is an exciting time to be a JavaScript developer. The
ecosystem is exploding with new tools, syntax, and best practices that
promise to solve many of our development problems. Our aim with this
book is to organize these techniques so you can get to work with React
right away. We’ll get into state management, React Router, testing, and
server rendering, so we promise not to introduce only the basics and
then throw you to the wolves.
This book does not assume any knowledge of React at all. We’ll
introduce all of React’s basics from scratch. Similarly, we won’t
assume that you’ve worked with the latest JavaScript syntax. This will
be introduced in Chapter 2 as a foundation for the rest of the chapters.
Along the way, check out the GitHub repository. All of the examples
are there and will allow you to practice hands-on.
Italic
Indicates new terms, URLs, email addresses, filenames, and file
extensions.
Constant width
Used for program listings, as well as within paragraphs to refer to
program elements such as variable or function names, databases,
data types, environment variables, statements, and keywords.
TIP
This element signifies a tip or suggestion.
NOTE
This element signifies a general note.
WARNING
This element indicates a warning or caution.
Using Code Examples
Supplemental material (code examples, exercises, etc.) is available for
download at https://github.com/moonhighway/learning-react.
This book is here to help you get your job done. In general, if example
code is offered with this book, you may use it in your programs and
documentation. You do not need to contact us for permission unless
you’re reproducing a significant portion of the code. For example,
writing a program that uses several chunks of code from this book does
not require permission. Selling or distributing examples from O’Reilly
books does require permission. Answering a question by citing this
book and quoting example code does not require permission.
Incorporating a significant amount of example code from this book into
your product’s documentation does require permission.
If you feel your use of code examples falls outside fair use or the
permission given above, feel free to contact us at
permissions@oreilly.com.
How to Contact Us
Please address comments and questions concerning this book to the
publisher:
Sebastopol, CA 95472
707-829-0104 (fax)
We have a web page for this book, where we list errata, examples, and
any additional information. You can access this page at
https://oreil.ly/learningReact_2e.
For news and information about our books and courses, visit
http://oreilly.com.
Acknowledgments
Our journey with React wouldn’t have started without some good old-
fashioned luck. We used YUI when we created the training materials
for the full-stack JavaScript program we taught internally at Yahoo.
Then in August 2014, development on YUI ended. We had to change
all our course files, but to what? What were we supposed to use on the
front-end now? The answer: React. We didn’t fall in love with React
immediately; it took us a couple hours to get hooked. It looked like
React could potentially change everything. We got in early and got
really lucky.
We appreciate the help of Angela Rufino and Jennifer Pollock for all
the support in developing this second edition. We also want to
acknowledge Ally MacDonald for all her editing help in the first
edition. We’re grateful to our tech reviewers, Scott Iwako, Adam
Rackis, Brian Sletten, Max Firtman, and Chetan Karande.
There’s also no way this book could have existed without Sharon
Adams and Marilyn Messineo. They conspired to purchase Alex’s first
computer, a Tandy TRS 80 Color Computer. It also wouldn’t have
made it to book form without the love, support, and encouragement of
Jim and Lorri Porcello and Mike and Sharon Adams.
When React was first released, there was a lot of conversation around
whether it was good, and there were many skeptics. It was new, and the
new can often be upsetting.
To respond to these critiques, Pete Hunt from the React team wrote an
article called “Why React?” that recommended that you “give it
[React] five minutes.” He wanted to encourage people to work with
React first before thinking that the team’s approach was too wild.
Yes, React is a small library that doesn’t come with everything you
might need out of the box to build your application. Give it five
minutes.
Yes, in React, you write code that looks like HTML right in your
JavaScript code. And yes, those tags require preprocessing to run in a
browser. And you’ll probably need a build tool like webpack for that.
Give it five minutes.
A Strong Foundation
Whether you’re brand new to React or looking to this text to learn
some of the latest features, we want this book to serve as a strong
foundation for all your future work with the library. The goal of this
book is to avoid confusion in the learning process by putting things in a
sequence: a learning roadmap.
Before digging into React, it’s important to know JavaScript. Not all of
JavaScript, not every pattern, but having a comfort with arrays, objects,
and functions before jumping into this book will be useful.
In the next chapter, we’ll look at newer JavaScript syntax to get you
acquainted with the latest JavaScript features, especially those that are
frequently used with React. Then we’ll give an introduction to
functional JavaScript so you can understand the paradigm that gave
birth to React. A nice side effect of working with React is that it can
make you a stronger JavaScript developer by promoting patterns that
are readable, reusable, and testable. Sort of like a gentle, helpful
brainwashing.
From there, we’ll cover foundational React knowledge to understand
how to build out a user interface with components. Then we’ll learn to
compose these components and add logic with props and state. We’ll
cover React Hooks, which allow us to reuse stateful logic between
components.
Once the basics are in place, we’ll build a new application that allows
users to add, edit, and delete colors. We’ll learn how Hooks and
Suspense can help us with data fetching. Throughout the construction
of that app, we’ll introduce a variety of tools from the broader React
ecosystem that are used to handle common concerns like routing,
testing, and server-side rendering.
Another huge event on the timeline was the release of React Fiber in
2017. Fiber was a rewrite of React’s rendering algorithm that was sort
of magical in its execution. It was a full rewrite of React’s internals that
changed barely anything about the public API. It was a way of making
React more modern and performant without affecting its users.
In the future, we’ll inevitably see more change, but one of the reasons
for React’s success is the strong team that has worked on the project
over the years. The team is ambitious yet cautious, pushing forward-
thinking optimizations while constantly considering the impact any
changes to the library will send cascading through the community.
As changes are made to React and related tools, sometimes there are
breaking changes. In fact, future versions of these tools may break
some of the example code in this book. You can still follow along with
the code samples. We’ll provide exact version information in the
package.json file so that you can install these packages at the correct
version.
Beyond this book, you can stay on top of changes by following along
with the official React blog. When new versions of React are released,
the core team will write a detailed blog post and changelog about
what’s new. The blog has also been translated into an ever-expanding
list of languages, so if English isn’t your native language, you can find
localized versions of the docs on the languages page of the docs site.
To install, head over to the GitHub repository. There, you’ll find links
to the Chrome and Firefox extensions.
Once installed, you’ll be able to see which sites are using React.
Anytime the React icon is illuminated in the browser toolbar as shown
in Figure 1-1, you’ll know that the site has React on the page.
Figure 1-1. Viewing the React Developer Tools in Chrome
Then, when you open the developer tools, there will be a new tab
visible called React, as shown in Figure 1-2. Clicking on that will show
all the components that make up the page you’re currently viewing.
Figure 1-2. Inspecting the DOM with the React Developer Tools
Installing Node.js
Node.js is a JavaScript runtime environment used to build full-stack
applications. Node is open source and can be installed on Windows,
macOS, Linux, and other platforms. We’ll be using Node in Chapter 12
when we build an Express server.
You need to have Node installed, but you do not need to be a Node
expert in order to use React. If you’re not sure if Node.js is installed on
your machine, you can open a Terminal or Command Prompt window
and type:
node -v
When you run this command, you should see a node version number
returned to you, ideally 8.6.2 or higher. If you type the command and
see an error message that says “Command not found,” Node.js is not
installed. This is easily fixed by installing Node.js from the Node.js
website. Just go through the installer’s automated steps, and when you
type in the node -v command again, you’ll see the version number.
NPM
When you installed Node.js, you also installed npm, the Node package
manager. In the JavaScript community, engineers share open source
code projects to avoid having to rewrite frameworks, libraries, or
helper functions on their own. React itself is an example of a useful
npm library. We’ll use npm to install a variety of packages throughout
this book.
If you’re starting your own project from scratch and want to include
dependencies, simply run the command:
npm init -y
This will initialize the project and create a package.json file. From
there, you can install your own dependencies with npm. To install a
package with npm, you’ll run:
YARN
Now that you have your environment set up for React development,
you’re ready to start walking the path of learning React. In Chapter 2,
we’ll get up to speed with the latest JavaScript syntax that’s most
commonly found in React code.
Chapter 2. JavaScript for
React
Since its release in 1995, JavaScript has gone through many changes.
At first, we used JavaScript to add interactive elements to web pages:
button clicks, hover states, form validation, etc.. Later, JavaScript got
more robust with DHTML and AJAX. Today, with Node.js, JavaScript
has become a real software language that’s used to build full-stack
applications. JavaScript is everywhere.
Since then, there has been a lot more momentum in this space. After
ES6 or ES2015 was released in, yes, 2015, there have been yearly
releases of new JS features. Anything that’s part of the stage proposals
is typically called ESNext, which is a simplified way of saying this is
the next stuff that will be part of the JavaScript spec.
Proposals are taken through clearly defined stages, from stage 0, which
represents the newest proposals, up through stage 4, which represents
the finished proposals. When a proposal gains traction, it’s up to the
browser vendors like Chrome and Firefox to implement the features.
Consider the const keyword. When creating variables, we used to use
var in all cases. The ECMA committee decided there should be a
const keyword to declare constants (more on that later in the chapter).
When const was first introduced, you couldn’t just write const in
JavaScript code and expect it to run in a browser. Now you can
because browser vendors have changed the browser to support it.
Many of the features we’ll discuss in this chapter are already supported
by the newest browsers, but we’ll also be covering how to compile
your JavaScript code. This is the process of transforming new syntax
that the browser doesn’t recognize into older syntax that the browser
understands. The kangax compatibility table is a great place to stay
informed about the latest JavaScript features and their varying degrees
of support by browsers.
In this chapter, we’ll show you all the JavaScript syntax we’ll be using
throughout the book. We hope to provide a good baseline of JavaScript
syntax knowledge that will carry you through all of your work with
React. If you haven’t made the switch to the latest syntax yet, now
would be a good time to get started. If you’re already comfortable with
the latest language features, skip to the next chapter.
Declaring Variables
Prior to ES2015, the only way to declare a variable was with the var
keyword. We now have a few different options that provide improved
functionality.
if (topic) {
var topic = "React";
console.log("block", topic); // block React
}
The topic variable inside the if block resets the value of topic
outside of the block.
With the let keyword, we can scope a variable to any code block.
Using let protects the value of the global variable:
if (topic) {
let topic = "React";
console.log("block", topic); // React
}
Another area where curly braces don’t block off a variable’s scope is in
for loops:
var div,
container = document.getElementById("container");
In this loop, we create five divs to appear within a container. Each div
is assigned an onclick handler that creates an alert box to display the
index. Declaring i in the for loop creates a global variable named i,
then iterates over it until its value reaches 5. When you click on any of
these boxes, the alert says that i is equal to 5 for all divs, because the
current value for the global i is 5 (see Figure 2-2).
Figure 2-2. i is equal to 5 for each box
Declaring the loop counter i with let instead of var does block off the
scope of i. Now clicking on any box will display the value for i that
was scoped to the loop iteration (see Figure 2-3):
With a template, we can create one string and insert the variable values
by surrounding them with ${ }:
const email = `
Hello ${firstName},
Order Details
${firstName} ${middleName} ${lastName}
${qty} x $${price} = $${qty*price} to ${event}
Thanks,
${ticketAgent}
`
document.body.innerHTML = `
<section>
<header>
<h1>The React Blog</h1>
</header>
<article>
<h2>${article.title}</h2>
${article.body}
</article>
<footer>
<p>copyright ${new Date().getYear()} | The React Blog</p>
</footer>
</section>
`;
Notice that we can include variables for the page title and article text as
well.
Creating Functions
Any time you want to perform some sort of repeatable task with
JavaScript, you can use a function. Let’s take a look at some of the
different syntax options that can be used to create a function and the
anatomy of those functions.
Function Declarations
A function declaration or function definition starts with the function
keyword, which is followed by the name of the function,
logCompliment. The JavaScript statements that are part of the function
are defined between the curly braces:
function logCompliment() {
console.log("You're doing great!");
}
function logCompliment() {
console.log("You're doing great!");
}
logCompliment();
Function Expressions
Another option is to use a function expression. This just involves
creating the function as a variable:
logCompliment();
The result is the same, and You're doing great! is logged to the
console.
This works. You’ll see the alert appear in the browser. It works
because the function is hoisted, or moved up, to the top of the file’s
scope. Trying the same exercise with a function expression will cause
an error:
PASSING ARGUMENTS
The logCompliment function currently takes in no arguments or
parameters. If we want to provide dynamic variables to the function,
we can pass named parameters to a function simply by adding them to
the parentheses. Let’s start by adding a firstName variable:
logCompliment("Molly");
FUNCTION RETURNS
Default Parameters
Languages including C++ and Python allow developers to declare
default values for function arguments. Default parameters are included
in the ES6 spec, so in the event that a value is not provided for the
argument, the default value will be used.
For example, we can set up default strings for the parameters name and
activity:
const defaultPerson = {
name: {
first: "Shane",
last: "McConkey"
},
favActivity: "skiing"
};
Arrow Functions
Arrow functions are a useful new feature of ES6. With arrow functions,
you can create functions without using the function keyword. You
also often do not have to use the return keyword. Let’s consider a
function that takes in a firstName and returns a string, turning the
person into a lord. Anyone can be a lord:
// Typical function
const lordify = function(firstName, land) {
return `${firstName} of ${land}`;
};
// Arrow Function
const lordify = (firstName, land) => `${firstName} of ${land}`;
if (!land) {
throw new Error("A lord must have a land");
}
RETURNING OBJECTS
console.log(person("Brad", "Janson"));
As soon as you run this, you’ll see the error: Uncaught SyntaxError:
Unexpected token :. To fix this, just wrap the object you’re
returning with parentheses:
console.log(person("Flad", "Hanson"));
const tahoe = {
mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
print: function(delay = 1000) {
setTimeout(function() {
console.log(this.mountains.join(", "));
}, delay);
}
};
This error is thrown because it’s trying to use the .join method on
what this is. If we log this, we’ll see that it refers to the Window
object:
console.log(this); // Window {}
To solve this problem, we can use the arrow function syntax to protect
the scope of this:
const tahoe = {
mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
print: function(delay = 1000) {
setTimeout(() => {
console.log(this.mountains.join(", "));
}, delay);
}
};
This works as expected, and we can .join the resorts with a comma.
Be careful that you’re always keeping scope in mind. Arrow functions
do not block off the scope of this:
const tahoe = {
mountains: ["Freel", "Rose", "Tallac", "Rubicon", "Silver"],
print: (delay = 1000) => {
setTimeout(() => {
console.log(this.mountains.join(", "));
}, delay);
}
};
Compiling JavaScript
When a new JavaScript feature is proposed and gains support, the
community often wants to use it before it’s supported by all browsers.
The only way to be sure that your code will work is to convert it to
more widely compatible code before running it in the browser. This
process is called compiling. One of the most popular tools for
JavaScript compilation is Babel.
In the past, the only way to use the latest JavaScript features was to
wait weeks, months, or even years until browsers supported them.
Now, Babel has made it possible to use the latest features of JavaScript
right away. The compiling step makes JavaScript similar to other
languages. It’s not quite traditional compiling: our code isn’t compiled
to binary. Instead, it’s transformed into syntax that can be interpreted
by a wider range of browsers. Also, JavaScript now has source code,
meaning that there will be some files that belong to your project that
don’t run in the browser.
"use strict";
A great way to learn more about how Babel works is to check out the
Babel REPL on the documentation website. Type some new syntax on
the left side, then see some older syntax created.
Destructuring Objects
Destructuring assignment allows you to locally scope fields within an
object and to declare which values will be used. Consider the
sandwich object. It has four keys, but we only want to use the values
of two. We can scope bread and meat to be used locally:
const sandwich = {
bread: "dutch crunch",
meat: "tuna",
cheese: "swiss",
toppings: ["lettuce", "tomato", "mustard"]
};
The code pulls bread and meat out of the object and creates local
variables for them. Also, since we declared these destructed variables
using let, the bread and meat variables can be changed without
changing the original sandwich:
const sandwich = {
bread: "dutch crunch",
meat: "tuna",
cheese: "swiss",
toppings: ["lettuce", "tomato", "mustard"]
};
console.log(bread); // garlic
console.log(meat); // turkey
const regularPerson = {
firstname: "Bill",
lastname: "Wilson"
};
const regularPerson = {
firstname: "Bill",
lastname: "Wilson"
};
Let’s take this one level farther to reflect a data change. Now, the
regularPerson object has a new nested object on the spouse key:
const regularPerson = {
firstname: "Bill",
lastname: "Wilson",
spouse: {
firstname: "Phil",
lastname: "Wilson"
}
};
Using the colon and nested curly braces, we can destructure the
firstname from the spouse object.
Destructuring Arrays
Values can also be destructured from arrays. Imagine that we wanted to
assign the first value of an array to a variable name:
console.log(firstAnimal); // Horse
We can also pass over unnecessary values with list matching using
commas. List matching occurs when commas take the place of
elements that should be skipped. With the same array, we can access
the last value by replacing the first two values with commas:
console.log(thirdAnimal); // Cat
// Old
var skier = {
name: name,
sound: sound,
powderYell: function() {
var yell = this.sound.toUpperCase();
console.log(`${yell} ${yell} ${yell}!!!`);
},
speed: function(mph) {
this.speed = mph;
console.log("speed:", mph);
}
};
// New
const skier = {
name,
sound,
powderYell() {
let yell = this.sound.toUpperCase();
console.log(`${yell} ${yell} ${yell}!!!`);
},
speed(mph) {
this.speed = mph;
console.log("speed:", mph);
}
};
All of the items from peaks and canyons are pushed into a new array
called tahoe.
Let’s take a look at how the spread operator can help us deal with a
problem. Using the peaks array from the previous sample, let’s
imagine that we wanted to grab the last item from the array rather than
the first. We could use the Array.reverse method to reverse the array
in combination with array destructuring:
console.log(last); // Rose
console.log(peaks.join(", ")); // Rose, Ralston, Tallac
console.log(last); // Rose
console.log(peaks.join(", ")); // Tallac, Ralston, Rose
Since we used the spread operator to copy the array, the peaks array is
still intact and can be used later in its original form.
The spread operator can also be used to get the remaining items in the
array:
function directions(...args) {
let [start, ...remaining] = args;
let [finish, ...stops] = remaining.reverse();
The spread operator can also be used for objects (see the GitHub page
for Rest/Spread Properties). Using the spread operator with objects is
similar to using it with arrays. In this example, we’ll use it the same
way we combined two arrays into a third array, but instead of arrays,
we’ll use objects:
const morning = {
breakfast: "oatmeal",
lunch: "peanut butter and jelly"
};
const backpackingMeals = {
...morning,
dinner
};
console.log(backpackingMeals);
// {
// breakfast: "oatmeal",
// lunch: "peanut butter and jelly",
// dinner: "mac and cheese"
// }
Asynchronous JavaScript
The code samples that have been part of this chapter so far have been
synchronous. When we write synchronous JavaScript code, we’re
providing a list of instructions that execute immediately in order. For
example, if we wanted to use JavaScript to handle some simple DOM
manipulation, we’d write the code to do so like this:
Let’s get some data from the randomuser.me API. This API has
information like email address, name, phone number, location, and so
on for fake members and is great to use as dummy data. fetch takes in
the URL for this resource as its only parameter:
console.log(fetch("https://api.randomuser.me/?nat=US&results=1"));
fetch("https://api.randomuser.me/?nat=US&results=1").then(res =>
console.log(res.json())
);
The then method will invoke the callback function once the promise
has resolved. Whatever you return from this function becomes the
argument of the next then function. So we can chain together then
functions to handle a promise that has been successfully resolved:
fetch("https://api.randomuser.me/?nat=US&results=1")
.then(res => res.json())
.then(json => json.results)
.then(console.log)
.catch(console.error);
Async/Await
Another popular approach for handling promises is to create an async
function. Some developers prefer the syntax of async functions because
it looks more familiar, like code that’s found in a synchronous
function. Instead of waiting for the results of a promise to resolve and
handling it with a chain of then functions, async functions can be told
to wait for the promise to resolve before further executing any code
found in the function.
Let’s make another API request but wrap the functionality with an
async function:
getFakePerson();
getFakePerson();
There we go—now this code accomplishes the exact same task as the
code in the previous section that uses then functions. If the fetch call
is successful, the results are logged to the console. If it’s unsuccessful,
then we’ll log the error to the console using console.error. When
using async and await, you need to surround your promise call in a
try…catch block to handle any errors that may occur due to an
unresolved promise.
Building Promises
When making an asynchronous request, one of two things can happen:
everything goes as we hope, or there’s an error. There can be many
different types of successful or unsuccessful requests. For example, we
could try several ways to obtain the data to reach success. We could
also receive multiple types of errors. Promises give us a way to
simplify back to a simple pass or fail.
With that, the promise has been created, but it hasn’t been used yet. We
can use the promise by calling the getPeople function and passing in
the number of members that should be loaded. The then function can
be chained on to do something once the promise has been fulfilled.
When a promise is rejected, any details are passed back to the catch
function, or the catch block if using async/await syntax:
getPeople(5)
.then(members => console.log(members))
.catch(error => console.error(`getPeople failed: ${error.message}`))
);
Classes
Prior to ES2015, there was no official class syntax in the JavaScript
spec. When classes were introduced, there was a lot of excitement
about how similar the syntax of classes was to traditional object-
oriented languages like Java and C++. The past few years saw the
React library leaning on classes heavily to construct user interface
components. Today, React is beginning to move away from classes,
instead using functions to construct components. You’ll still see classes
all over the place, particularly in legacy React code and in the world of
JavaScript, so let’s take a quick look at them.
Vacation.prototype.print = function() {
console.log(this.destination + " | " + this.length + " days");
};
This code creates something that feels like a custom type in an object-
oriented language. A Vacation has properties (destination, length), and
it has a method (print). The maui instance inherits the print method
through the prototype. If you are or were a developer accustomed to
more standard classes, this might fill you with a deep rage. ES2015
introduced class declaration to quiet that rage, but the dirty secret is
that JavaScript still works the same way. Functions are objects, and
inheritance is handled through the prototype. Classes provide a
syntactic sugar on top of that gnarly prototype syntax:
class Vacation {
constructor(destination, length) {
this.destination = destination;
this.length = length;
}
print() {
console.log(`${this.destination} will take ${this.length} days.`);
}
}
Now that a class object has been created, you can use it as many times
as you’d like to create new vacation instances. Classes can also be
extended. When a class is extended, the subclass inherits the properties
and methods of the superclass. These properties and methods can be
manipulated from here, but as a default, all will be inherited.
print() {
super.print();
console.log(`Bring your ${this.gear.join(" and your ")}`);
}
}
trip.print();
ES6 Modules
A JavaScript module is a piece of reusable code that can easily be
incorporated into other JavaScript files without causing variable
collisions. JavaScript modules are stored in separate files, one file per
module. There are two options when creating and exporting a module:
you can export multiple JavaScript objects from a single module or one
JavaScript object per module.
print("printing a message");
log("logging a message");
freel.print();
p("printing a message");
l("logging a message");
CommonJS
CommonJS is the module pattern that’s supported by all versions of
Node (see the Node.js documentation on modules). You can still use
these modules with Babel and webpack. With CommonJS, JavaScript
objects are exported using module.exports.
When you start to explore React, you’ll likely notice that the topic of
functional programming comes up a lot. Functional techniques are
being used more and more in JavaScript projects, particularly React
projects.
If you’re wondering where this functional trend came from, the answer
is the 1930s, with the invention of lambda calculus, or λ-calculus.1
Functions have been a part of calculus since it emerged in the 17th
century. Functions can be sent to functions as arguments or returned
from functions as results. More complex functions, called higher-
order functions, can manipulate functions and use them as either
arguments or results or both. In the 1930s, Alonzo Church was at
Princeton experimenting with these higher-order functions when he
invented lambda calculus.
In the late 1950s, John McCarthy took the concepts derived from λ-
calculus and applied them to a new programming language called Lisp.
Lisp implemented the concept of higher-order functions and functions
as first-class members or first-class citizens. A function is considered a
first-class member when it can be declared as a variable and sent to
functions as an argument. These functions can even be returned from
functions.
const obj = {
message: "They can be added to objects like variables",
log(message) {
console.log(message);
}
};
obj.log(obj.message);
const messages = [
"They can be inserted into arrays",
message => console.log(message),
"like variables",
message => console.log(message)
];
messages[1](messages[0]); // They can be inserted into arrays
messages[3](messages[2]); // like variables
They can also be returned from other functions, just like variables:
If you see more than one arrow used during a function declaration, this
means that you’re using a higher-order function.
console.log(urlFriendly); // "Restaurants-in-Hanalei"
console.log(urlFriendly);
Declarative programs are easy to reason about because the code itself
describes what is happening. For example, read the syntax in the
following sample. It details what happens after members are loaded
from an API:
getFakeMembers(100).then(loadAndMapMembers);
wrapper.id = "welcome";
headline.innerText = "Hello World";
wrapper.appendChild(headline);
target.appendChild(wrapper);
Immutability
To mutate is to change, so to be immutable is to be unchangeable. In a
functional program, data is immutable. It never changes.
If you need to share your birth certificate with the public but want to
redact or remove private information, you essentially have two choices:
you can take a big Sharpie to your original birth certificate and cross
out your private data, or you can find a copy machine. Finding a copy
machine, making a copy of your birth certificate, and writing all over
that copy with that big Sharpie would be preferable. This way you can
have a redacted birth certificate to share and your original that’s still
intact.
We could build a function that would rate colors and use that function
to change the rating of the color object:
console.log(rateColor(color_lawn, 5).rating); // 5
console.log(color_lawn.rating); // 5
console.log(rateColor(color_lawn, 5).rating); // 5
console.log(color_lawn.rating); // 0
Here, we used Object.assign to change the color rating.
Object.assign is the copy machine. It takes a blank object, copies the
color to that object, and overwrites the rating on the copy. Now we can
have a newly rated color object without having to change the original.
We can write the same function using an arrow function along with the
object spread operator. This rateColor function uses the spread
operator to copy the color into a new object and then overwrite its
rating:
We could create a function that will add colors to that array using
Array.push:
You can also use the spread operator to concatenate arrays in the same
way it can be used to copy objects. Here’s the emerging JavaScript
equivalent of the previous addColor function:
This function copies the original list to a new array and then adds a
new object containing the color’s title to that copy. It is immutable.
Pure Functions
A pure function is a function that returns a value that’s computed based
on its arguments. Pure functions take at least one argument and always
return a value or another function. They do not cause side effects, set
global variables, or change anything about application state. They treat
their arguments as immutable data.
const frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
};
function selfEducate() {
frederick.canRead = true;
frederick.canWrite = true;
return frederick;
}
selfEducate();
console.log(frederick);
The selfEducate function is not a pure function. It does not take any
arguments, and it does not return a value or a function. It also changes
a variable outside of its scope: Frederick. Once the selfEducate
function is invoked, something about the “world” has changed. It
causes side effects:
const frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
};
console.log(selfEducate(frederick));
console.log(frederick);
const frederick = {
name: "Frederick Douglass",
canRead: false,
canWrite: false
};
console.log(selfEducate(frederick));
console.log(frederick);
// {name: "Frederick Douglass", canRead: true, canWrite: true}
// {name: "Frederick Douglass", canRead: false, canWrite: false}
function Header(text) {
let h1 = document.createElement("h1");
h1.innerText = text;
document.body.appendChild(h1);
}
Data Transformations
How does anything change in an application if the data is immutable?
Functional programming is all about transforming data from one form
to another. We’ll produce transformed copies using functions. These
functions make our code less imperative and thus reduce complexity.
In this section, we’ll take a look at how these and some other core
functions transform data from one type to another.
console.log(wSchools);
// ["Washington & Liberty", "Wakefield"]
// "Yorktown, Wakefield"
console.log(schools.join("\n"));
// Yorktown
// Washington & Liberty
// Wakefield
In this case, the cutSchool function is used to return a new array that
does not contain “Washington & Liberty.” Then, the join function is
used with this new array to create a string out of the remaining two
school names. cutSchool is a pure function. It takes a list of schools
and the name of the school that should be removed and returns a new
array without that specific school.
console.log(highSchools.join("\n"));
console.log(schools.join("\n"));
// Yorktown
// Washington & Liberty
// Wakefield
In this case, the map function was used to append “High School” to
each school name. The schools array is still intact.
console.log(highSchools);
// [
// { name: "Yorktown" },
// { name: "Washington & Liberty" },
// { name: "Wakefield" }
// ]
If you need to create a pure function that changes one object in an array
of objects, map can be used for this, too. In the following example,
we’ll change the school with the name of “Stratford” to “HB
Woodlawn” without mutating the schools array:
let schools = [
{ name: "Yorktown" },
{ name: "Stratford" },
{ name: "Washington & Liberty" },
{ name: "Wakefield" }
];
const schools = {
Yorktown: 10,
"Washington & Liberty": 2,
Wakefield: 5
};
console.log(schoolArray);
// [
// {
// name: "Yorktown",
// wins: 10
// },
// {
// name: "Washington & Liberty",
// wins: 2
// },
// {
// name: "Wakefield",
// wins: 5
// }
// ]
So far, we’ve learned that we can transform arrays with Array.map and
Array.filter. We’ve also learned that we can change arrays into
objects by combining Object.keys with Array.map. The final tool
that we need in our functional arsenal is the ability to transform arrays
into primitives and other objects.
console.log("maxAge", maxAge);
// 21 > 0 = true
// 18 > 21 = false
// 42 > 21 = true
// 40 > 42 = false
// 64 > 42 = true
// 63 > 64 = false
// 34 > 64 = false
// maxAge 64
The ages array has been reduced into a single value: the maximum
age, 64. reduce takes two arguments: a callback function and an
original value. In this case, the original value is 0, which sets the initial
maximum value to 0. The callback is invoked once for every item in
the array. The first time this callback is invoked, age is equal to 21, the
first value in the array, and max is equal to 0, the initial value. The
callback returns the greater of the two numbers, 21, and that becomes
the max value during the next iteration. Each iteration compares each
age against the max value and returns the greater of the two. Finally,
the last number in the array is compared and returned from the
previous callback.
const max = ages.reduce((max, value) => (value > max ? value : max), 0);
ARRAY.REDUCERIGHT
Array.reduceRight works the same way as Array.reduce; the difference is that
it starts reducing from the end of the array rather than the beginning.
const colors = [
{
id: "xekare",
title: "rad red",
rating: 3
},
{
id: "jbwsof",
title: "big blue",
rating: 2
},
{
id: "prigbj",
title: "grizzly grey",
rating: 5
},
{
id: "ryhbhsl",
title: "banana",
rating: 1
}
];
console.log(hashColors);
// {
// "xekare": {
// title:"rad red",
// rating:3
// },
// "jbwsof": {
// title:"big blue",
// rating:2
// },
// "prigbj": {
// title:"grizzly grey",
// rating:5
// },
// "ryhbhsl": {
// title:"banana",
// rating:1
// }
// }
console.log(uniqueColors);
map and reduce are the main weapons of any functional programmer,
and JavaScript is no exception. If you want to be a proficient
JavaScript engineer, then you must master these functions. The ability
to create one dataset from another is a required skill and is useful for
any type of programming paradigm.
Higher-Order Functions
The use of higher-order functions is also essential to functional
programming. We’ve already mentioned higher-order functions, and
we’ve even used a few in this chapter. Higher-order functions are
functions that can manipulate other functions. They can take functions
in as arguments or return functions or both.
Recursion
Recursion is a technique that involves creating functions that recall
themselves. Often, when faced with a challenge that involves a loop, a
recursive function can be used instead. Consider the task of counting
down from 10. We could create a for loop to solve this problem, or we
could alternatively use a recursive function. In this example,
countdown is the recursive function:
// 10
// 9
// 8
// 7
// 6
// 5
// 4
// 3
// 2
// 1
// 0
const dan = {
type: "person",
data: {
gender: "male",
info: {
id: 22,
fullname: {
first: "Dan",
last: "Deacon"
}
}
}
};
This function continues to call itself until the fields string no longer
contains dots, meaning that there are no more remaining fields. In this
sample, you can see how the values for first, remaining, and
object[first] change as deepPick iterates through:
// First Iteration
// first = "data"
// remaining.join(".") = "info.fullname.first"
// object[first] = { gender: "male", {info} }
// Second Iteration
// first = "info"
// remaining.join(".") = "fullname.first"
// object[first] = {id: 22, {fullname}}
// Third Iteration
// first = "fullname"
// remaining.join("." = "first"
// object[first] = {first: "Dan", last: "Deacon" }
// Finally...
// first = "first"
// remaining.length = 0
// object[first] = "Deacon"
Composition
Functional programs break up their logic into small, pure functions that
are focused on specific tasks. Eventually, you’ll need to put these
smaller functions together. Specifically, you may need to combine
them, call them in series or parallel, or compose them into larger
functions until you eventually have an application.
console.log(clockTime);
// "03:33:33 PM"
The both function is one function that pipes a value through two
separate functions. The output of civilian hours becomes the input
for appendAMPM, and we can change a date using both of these
functions combined into one:
both(new Date());
This approach looks much better. It’s easy to scale because we can add
more functions at any point. This approach also makes it easy to
change the order of the composed functions.
function logClockTime() {
// Get Time string as civilian time
let time = getClockTime();
function getClockTime() {
// Get the Current Time
let date = new Date();
let time = "";
Our goal will be to break the application logic up into smaller parts:
functions. Each function will be focused on a single task, and we’ll
compose them into larger functions that we can use to create the clock.
First, let’s create some functions that give us values and manage the
console. We’ll need a function that gives us one second, a function that
gives us the current time, and a couple of functions that will log
messages on a console and clear the console. In functional programs,
we should use functions over values wherever possible. We’ll invoke
the function to obtain the value when needed:
Next, we’ll need some functions for transforming data. These three
functions will be used to mutate the Date object into an object that can
be used for our clock:
serializeClockTime
Takes a date object and returns an object for clock time that
contains hours, minutes, and seconds.
civilianHours
Takes the clock time object and returns an object where hours are
converted to civilian time. For example: 1300 becomes 1:00.
appendAMPM
Takes the clock time object and appends time of day (AM or PM)
to that object.
These three functions are used to transform data without changing the
original. They treat their arguments as immutable objects.
Next, we’ll need a few higher-order functions:
display
Takes a target function and returns a function that will send a time
to the target. In this example, the target will be console.log.
formatClock
Takes a template string and uses it to return clock time formatted
based on the criteria from the string. In this example, the template
is “hh:mm:ss tt”. From there, formatClock will replace the
placeholders with hours, minutes, seconds, and time of day.
prependZero
Takes an object’s key as an argument and prepends a zero to the
value stored under that object’s key. It takes in a key to a specific
field and prepends values with a zero if the value is less than 10.
Now that we have all of the functions required to build a ticking clock,
we’ll need to compose them. We’ll use the compose function that we
defined in the last section to handle composition:
convertToCivilianTime
A single function that takes clock time as an argument and
transforms it into civilian time by using both civilian hours.
doubleDigits
A single function that takes civilian clock time and makes sure the
hours, minutes, and seconds display double digits by prepending
zeros where needed.
startTicking
Starts the clock by setting an interval that invokes a callback every
second. The callback is composed using all our functions. Every
second the console is cleared, currentTime is obtained, converted,
civilianized, formatted, and displayed.
startTicking();
This declarative version of the clock achieves the same results as the
imperative version. However, there quite a few benefits to this
approach. First, all of these functions are easily testable and reusable.
They can be used in future clocks or other digital displays. Also, this
program is easily scalable. There are no side effects. There are no
global variables outside of functions themselves. There could still be
bugs, but they’ll be easier to find.
When you work with React, it’s more than likely that you’ll be creating
your apps with JSX. JSX is a tag-based JavaScript syntax that looks a
lot like HTML. It’s a syntax we’ll dive deep into in the next chapter
and continue to use for the rest of the book. To truly understand React,
though, we need to understand its most atomic units: React elements.
From there, we’ll get into React elements. From there, we’ll get into
React components by looking at how we can create custom
components that compose other components and elements.
Page Setup
In order to work with React in the browser, we need to include two
libraries: React and ReactDOM. React is the library for creating views.
ReactDOM is the library used to actually render the UI in the browser.
Both libraries are available as scripts from the unpkg CDN (links are
included in the following code). Let’s set up an HTML document:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Samples</title>
</head>
<body>
<!-- Target container -->
<div id="root"></div>
<script>
// Pure React and JavaScript code
</script>
</body>
</html>
These are the minimum requirements for working with React in the
browser. You can place your JavaScript in a separate file, but it must
be loaded somewhere in the page after React has been loaded. We’re
going to be using the development version of React to see all of the
error messages and warnings in the browser console. You can choose
to use the minified production version using react.production.min.js
and react-dom.production.min.js, which will strip away those
warnings.
React Elements
HTML is simply a set of instructions that a browser follows when
constructing the DOM. The elements that make up an HTML
document become DOM elements when the browser loads HTML and
renders the user interface.
Let’s say you have to construct an HTML hierarchy for a recipe. A
possible solution for such a task might look something like this:
<section id="baked-salmon">
<h1>Baked Salmon</h1>
<ul class="ingredients">
<li>2 lb salmon</li>
<li>5 sprigs fresh rosemary</li>
<li>2 tablespoons olive oil</li>
<li>2 small lemons</li>
<li>1 teaspoon kosher salt</li>
<li>4 cloves of chopped garlic</li>
</ul>
<section class="instructions">
<h2>Cooking Instructions</h2>
<p>Preheat the oven to 375 degrees.</p>
<p>Lightly coat aluminum foil with oil.</p>
<p>Place salmon on foil</p>
<p>Cover with rosemary, sliced lemons, chopped garlic.</p>
<p>Bake for 15-20 minutes until cooked through.</p>
<p>Remove from oven.</p>
</section>
</section>
React is a library that’s designed to update the browser DOM for us.
We no longer have to be concerned with the complexities associated
with building high-performing SPAs because React can do that for us.
With React, we do not interact with the DOM API directly. Instead, we
provide instructions for what we want React to build, and React will
take care of rendering and reconciling the elements we’ve instructed it
to create.
The properties are similarly applied to the new DOM element: the
properties are added to the tag as attributes, and the child text is added
as text within the element. A React element is just a JavaScript literal
that tells React how to construct the DOM element.
{
$$typeof: Symbol(React.element),
"type": "h1",
"key": null,
"ref": null,
"props": {id: "recipe-0", children: "Baked Salmon"},
"_owner": null,
"_store": {}
}
This is the structure of a React element. There are fields that are used
by React: _owner, _store, and $$typeof. The key and ref fields are
important to React elements, but we’ll introduce those later. For now,
let’s take a closer look at the type and props fields.
The type property of the React element tells React what type of
HTML or SVG element to create. The props property represents the
data and child elements required to construct a DOM element. The
children property is for displaying other nested elements as text.
CREATING ELEMENTS
We’re taking a peek at the object that React.createElement returns. You won’t
actually create these elements by hand; instead, you’ll use the
React.createElement function.
ReactDOM
Once we’ve created a React element, we’ll want to see it in the
browser. ReactDOM contains the tools necessary to render React
elements in the browser. ReactDOM is where we’ll find the render
method.
ReactDOM.render(dish, document.getElementById("root"));
Rendering the title element to the DOM would add an h1 element to
the div with the id of root, which we would already have defined in
our HTML. We build this div inside the body tag:
<body>
<div id="root">
<h1>Baked Salmon</h1>
</div>
</body>
This will render both of these elements as siblings inside of the root
container. We hope you just clapped and screamed!
Children
React renders child elements using props.children. In the previous
section, we rendered a text element as a child of the h1 element, and
thus props.children was set to Baked Salmon. We could render
other React elements as children, too, creating a tree of elements. This
is why we use the term element tree: the tree has one root element from
which many branches grow.
<ul>
<li>2 lb salmon</li>
<li>5 sprigs fresh rosemary</li>
<li>2 tablespoons olive oil</li>
<li>2 small lemons</li>
<li>1 teaspoon kosher salt</li>
<li>4 cloves of chopped garlic</li>
</ul>
In this sample, the unordered list is the root element, and it has six
children. We can represent this ul and its children with
React.createElement:
React.createElement(
"ul",
null,
React.createElement("li", null, "2 lb salmon"),
React.createElement("li", null, "5 sprigs fresh rosemary"),
React.createElement("li", null, "2 tablespoons olive oil"),
React.createElement("li", null, "2 small lemons"),
React.createElement("li", null, "1 teaspoon kosher salt"),
React.createElement("li", null, "4 cloves of chopped garlic")
);
console.log(list);
{
"type": "ul",
"props": {
"children": [
{ "type": "li", "props": { "children": "2 lb salmon" } … },
{ "type": "li", "props": { "children": "5 sprigs fresh rosemary"} …
},
{ "type": "li", "props": { "children": "2 tablespoons olive oil" } …
},
{ "type": "li", "props": { "children": "2 small lemons"} … },
{ "type": "li", "props": { "children": "1 teaspoon kosher salt"} … },
{ "type": "li", "props": { "children": "4 cloves of chopped garlic"}
… }
]
...
}
}
We can now see that each list item is a child. Earlier in this chapter, we
introduced HTML for an entire recipe rooted in a section element. To
create this using React, we’ll use a series of createElement calls:
React.createElement(
"section",
{ id: "baked-salmon" },
React.createElement("h1", null, "Baked Salmon"),
React.createElement(
"ul",
{ className: "ingredients" },
React.createElement("li", null, "2 lb salmon"),
React.createElement("li", null, "5 sprigs fresh rosemary"),
React.createElement("li", null, "2 tablespoons olive oil"),
React.createElement("li", null, "2 small lemons"),
React.createElement("li", null, "1 teaspoon kosher salt"),
React.createElement("li", null, "4 cloves of chopped garlic")
),
React.createElement(
"section",
{ className: "instructions" },
React.createElement("h2", null, "Cooking Instructions"),
React.createElement("p", null, "Preheat the oven to 375 degrees."),
React.createElement("p", null, "Lightly coat aluminum foil with
oil."),
React.createElement("p", null, "Place salmon on foil."),
React.createElement(
"p",
null,
"Cover with rosemary, sliced lemons, chopped garlic."
),
React.createElement(
"p",
null,
"Bake for 15-20 minutes until cooked through."
),
React.createElement("p", null, "Remove from oven.")
)
);
CLASSNAME IN REACT
Any element that has an HTML class attribute is using className for that
property instead of class. Since class is a reserved word in JavaScript, we have
to use className to define the class attribute of an HTML element. This sample
is what pure React looks like. Pure React is ultimately what runs in the browser. A
React app is a tree of React elements all stemming from a single root element.
React elements are the instructions React will use to build a UI in the browser.
The major advantage of using React is its ability to separate data from
UI elements. Since React is just JavaScript, we can add JavaScript
logic to help us build the React component tree. For example,
ingredients can be stored in an array, and we can map that array to the
React elements.
Let’s go back and think about the unordered list for a moment:
React.createElement(
"ul",
null,
React.createElement("li", null, "2 lb salmon"),
React.createElement("li", null, "5 sprigs fresh rosemary"),
React.createElement("li", null, "2 tablespoons olive oil"),
React.createElement("li", null, "2 small lemons"),
React.createElement("li", null, "1 teaspoon kosher salt"),
React.createElement("li", null, "4 cloves of chopped garlic")
);
The data used in this list of ingredients can easily be represented using
a JavaScript array:
const items = [
"2 lb salmon",
"5 sprigs fresh rosemary",
"2 tablespoons olive oil",
"2 small lemons",
"1 teaspoon kosher salt",
"4 cloves of chopped garlic"
];
We want to use this data to generate the correct number of list items
without having to hard-code each one. We can map over the array and
create list items for as many ingredients as there are:
React.createElement(
"ul",
{ className: "ingredients" },
items.map(ingredient => React.createElement("li", null, ingredient))
);
This syntax creates a React element for each ingredient in the array.
Each string is displayed in the list item’s children as text. The value for
each ingredient is displayed as the list item.
When running this code, you’ll see a console warning like the one
shown in Figure 4-1.
React.createElement(
"ul",
{ className: "ingredients" },
items.map((ingredient, i) =>
React.createElement("li", { key: i }, ingredient)
)
);
We’ll cover keys in more detail when we discuss JSX, but adding this
to each list item will clear the console warning.
React Components
No matter its size, its contents, or what technologies are used to create
it, a user interface is made up of parts. Buttons. Lists. Headings. All of
these parts, when put together, make up a user interface. Consider a
recipe application with three different recipes. The data is different in
each box, but the parts needed to create a recipe are the same (see
Figure 4-2).
Figure 4-2. Recipes app
When considering a user interface you want to build with React, look
for opportunities to break down your elements into reusable pieces. For
example, the recipes in Figure 4-3 have a title, ingredients list, and
instructions. All are part of a larger recipe or app component. We could
create a component for each of the highlighted parts: ingredients,
instructions, and so on.
Figure 4-3. Each component is outlined: App, IngredientsList, Instructions
Think about how scalable this is. If we want to display one recipe, our
component structure will support this. If we want to display 10,000
recipes, we’ll just create 10,000 new instances of that component.
function IngredientsList() {
return React.createElement(
"ul",
{ className: "ingredients" },
React.createElement("li", null, "1 cup unsalted butter"),
React.createElement("li", null, "1 cup crunchy peanut butter"),
React.createElement("li", null, "1 cup brown sugar"),
React.createElement("li", null, "1 cup white sugar"),
React.createElement("li", null, "2 eggs"),
React.createElement("li", null, "2.5 cups all purpose flour"),
React.createElement("li", null, "1 teaspoon baking powder"),
React.createElement("li", null, "0.5 teaspoon salt")
);
}
ReactDOM.render(
React.createElement(IngredientsList, null, null),
document.getElementById("root")
);
<IngredientsList>
<ul className="ingredients">
<li>1 cup unsalted butter</li>
<li>1 cup crunchy peanut butter</li>
<li>1 cup brown sugar</li>
<li>1 cup white sugar</li>
<li>2 eggs</li>
<li>2.5 cups all purpose flour</li>
<li>1 teaspoon baking powder</li>
<li>0.5 teaspoon salt</li>
</ul>
</IngredientsList>
This is pretty cool, but we’ve hardcoded this data into the component.
What if we could build one component and then pass data into that
component as properties? And then what if that component could
render the data dynamically? Maybe someday that will happen!
const secretIngredients = [
"1 cup unsalted butter",
"1 cup crunchy peanut butter",
"1 cup brown sugar",
"1 cup white sugar",
"2 eggs",
"2.5 cups all purpose flour",
"1 teaspoon baking powder",
"0.5 teaspoon salt"
];
function IngredientsList() {
return React.createElement(
"ul",
{ className: "ingredients" },
items.map((ingredient, i) =>
React.createElement("li", { key: i }, ingredient)
)
);
}
ReactDOM.render(
React.createElement(IngredientsList, { items: secretIngredients },
null),
document.getElementById("root")
);
Now, let’s look at the DOM. The data property items is an array with
eight ingredients. Because we made the li tags using a loop, we were
able to add a unique key using the index of the loop:
<IngredientsList items="[...]">
<ul className="ingredients">
<li key="0">1 cup unsalted butter</li>
<li key="1">1 cup crunchy peanut butter</li>
<li key="2">1 cup brown sugar</li>
<li key="3">1 cup white sugar</li>
<li key="4">2 eggs</li>
<li key="5">2.5 cups all purpose flour</li>
<li key="6">1 teaspoon baking powder</li>
<li key="7">0.5 teaspoon salt</li>
</ul>
</IngredientsList>
Creating our component this way will make the component more
flexible. Whether the items array is one item or a hundred items long,
the component will render each as a list item.
function IngredientsList(props) {
return React.createElement(
"ul",
{ className: "ingredients" },
props.items.map((ingredient, i) =>
React.createElement("li", { key: i }, ingredient)
)
);
}
When React was first made open source in 2013, there was one way to
create a component: createClass. The use of React.createClass to
create a component looks like this:
It’s still possible to create a React component using class syntax, but be
forewarned that React.Component is on the path to deprecation as
well. Although it’s still supported, you can expect this to go the way of
React.createClass, another old friend who shaped you but who you
won’t see as often because they moved away and you moved on. From
now on, we’ll use functions to create components in this book and only
briefly point out older patterns for reference.
Chapter 5. React with JSX
In the last chapter, we dove deep into how React works, breaking down
our React applications into small reusable pieces called components.
These components render trees of elements and other components.
Using the createElement function is a good way to see how React
works, but as React developers, that’s not what we do. We don’t go
around composing complex, barely readable trees of JavaScript syntax
and call it fun. In order to work efficiently with React, we need one
more thing: JSX.
You can also add other JSX elements as children. If you have an
unordered list, you can add child list item elements to it with JSX tags.
It looks very similar to HTML:
<ul>
<li>1 lb Salmon</li>
<li>1 cup Pine Nuts</li>
<li>2 cups Butter Lettuce</li>
<li>1 Yellow Squash</li>
<li>1/2 cup Olive Oil</li>
<li>3 Cloves of Garlic</li>
</ul>
JSX Tips
JSX might look familiar, and most of the rules result in syntax that’s
similar to HTML. However, there are a few considerations you should
understand when working with JSX.
NESTED COMPONENTS
<IngredientsList>
<Ingredient />
<Ingredient />
<Ingredient />
</IngredientsList>
CLASSNAME
JAVASCRIPT EXPRESSIONS
<h1>{title}</h1>
EVALUATION
The JavaScript that’s added in between the curly braces will get
evaluated. This means that operations such as concatenation or addition
will occur. This also means that functions found in JavaScript
expressions will be invoked:
<h1>{"Hello" + title}</h1>
<h1>{title.toLowerCase().replace}</h1>
<ul>
{props.ingredients.map((ingredient, i) => (
<li key="{i}">{ingredient}</li>
))}
</ul>
JSX looks clean and readable, but it can’t be interpreted with a
browser. All JSX must be converted into createElement calls.
Luckily, there’s an excellent tool for this task: Babel.
Babel
Many software languages require you to compile your source code.
JavaScript is an interpreted language: the browser interprets the code as
text, so there’s no need to compile JavaScript. However, not all
browsers support the latest JavaScript syntax, and no browser supports
JSX syntax. Since we want to use the latest features of JavaScript along
with JSX, we’re going to need a way to convert our fancy source code
into something that the browser can interpret. This process is called
compiling, and it’s what Babel is designed to do.
The first version of the project was called 6to5, and it was released in
September 2014. 6to5 was a tool that could be used to convert ES6
syntax to ES5 syntax, which was more widely supported by web
browsers. As the project grew, it aimed to be a platform to support all
of the latest changes in ECMAScript. It also grew to support
converting JSX into JavaScript. The project was renamed Babel in
February 2015.
There are many ways of working with Babel. The easiest way to get
started is to include a link to the Babel CDN directly in your HTML,
which will compile any code in script blocks that have a type of
“text/babel.” Babel will compile the source code on the client before
running it. Although this may not be the best solution for production,
it’s a great way to get started with JSX:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Examples</title>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
// JSX code here. Or link to separate JavaScript file that contains
JSX.
</script>
</body>
</html>
This data array contains two recipes, and this represents our
application’s current state:
const data = [
{
name: "Baked Salmon",
ingredients: [
{ name: "Salmon", amount: 1, measurement: "l lb" },
{ name: "Pine Nuts", amount: 1, measurement: "cup" },
{ name: "Butter Lettuce", amount: 2, measurement: "cups" },
{ name: "Yellow Squash", amount: 1, measurement: "med" },
{ name: "Olive Oil", amount: 0.5, measurement: "cup" },
{ name: "Garlic", amount: 3, measurement: "cloves" }
],
steps: [
"Preheat the oven to 350 degrees.",
"Spread the olive oil around a glass baking dish.",
"Add the yellow squash and place in the oven for 30 mins.",
"Add the salmon, garlic, and pine nuts to the dish.",
"Bake for 15 minutes.",
"Remove from oven. Add the lettuce and serve."
]
},
{
name: "Fish Tacos",
ingredients: [
{ name: "Whitefish", amount: 1, measurement: "l lb" },
{ name: "Cheese", amount: 1, measurement: "cup" },
{ name: "Iceberg Lettuce", amount: 2, measurement: "cups" },
{ name: "Tomatoes", amount: 2, measurement: "large" },
{ name: "Tortillas", amount: 3, measurement: "med" }
],
steps: [
"Cook the fish on the grill until cooked through.",
"Place the fish on the 3 tortillas.",
"Top them with lettuce, tomatoes, and cheese."
]
}
];
The React elements within the Menu component are expressed as JSX.
Everything is contained within an article element. A header
element, an h1 element, and a div.recipes element are used to
describe the DOM for our menu. The value for the title property will
be displayed as text within the h1:
function Menu(props) {
return (
<article>
<header>
<h1>{props.title}</h1>
</header>
<div className="recipes" />
</article>
);
}
<div className="recipes">
{props.recipes.map((recipe, i) => (
<Recipe
key={i}
name={recipe.name}
ingredients={recipe.ingredients}
steps={recipe.steps}
/>
))}
</div>
You could also refactor this to use spread syntax. The JSX spread
operator works like the object spread operator. It will add each field of
the recipe object as a property of the Recipe component. The syntax
here will supply all properties to the component:
{
props.recipes.map((recipe, i) => <Recipe key={i} {...recipe} />);
}
Remember that this shortcut will provide all the properties to the
Recipe component. This could be a good thing but might also add too
many properties to the component.
Each recipe has a string for the name, an array of objects for
ingredients, and an array of strings for the steps. Using object
destructuring, we can tell this component to locally scope those fields
by name so we can access them directly without having to use
props.name, props.ingredients, or props.steps.
The complete code for the application should look like this:
const data = [
{
name: "Baked Salmon",
ingredients: [
{ name: "Salmon", amount: 1, measurement: "l lb" },
{ name: "Pine Nuts", amount: 1, measurement: "cup" },
{ name: "Butter Lettuce", amount: 2, measurement: "cups" },
{ name: "Yellow Squash", amount: 1, measurement: "med" },
{ name: "Olive Oil", amount: 0.5, measurement: "cup" },
{ name: "Garlic", amount: 3, measurement: "cloves" }
],
steps: [
"Preheat the oven to 350 degrees.",
"Spread the olive oil around a glass baking dish.",
"Add the yellow squash and place in the oven for 30 mins.",
"Add the salmon, garlic, and pine nuts to the dish.",
"Bake for 15 minutes.",
"Remove from oven. Add the lettuce and serve."
]
},
{
name: "Fish Tacos",
ingredients: [
{ name: "Whitefish", amount: 1, measurement: "l lb" },
{ name: "Cheese", amount: 1, measurement: "cup" },
{ name: "Iceberg Lettuce", amount: 2, measurement: "cups" },
{ name: "Tomatoes", amount: 2, measurement: "large" },
{ name: "Tortillas", amount: 3, measurement: "med" }
],
steps: [
"Cook the fish on the grill until hot.",
"Place the fish on the 3 tortillas.",
"Top them with lettuce, tomatoes, and cheese."
]
}
];
ReactDOM.render(
<Menu recipes={data} title="Delicious Recipes" />,
document.getElementById("root")
);
When we run this code in the browser, React will construct a UI using
our instructions with the recipe data as shown in Figure 5-2.
If you’re using Google Chrome and have the React Developer Tools
Extension installed, you can take a look at the present state of the
component tree. To do this, open the developer tools and select the
Components tab, as shown in Figure 5-3.
Here we can see the Menu and its child elements. The data array
contains two objects for recipes, and we have two Recipe elements.
Each Recipe element has properties for the recipe name, ingredients,
and steps. The ingredients and steps are passed down to their own
components as data.
React Fragments
In the previous section, we rendered the Menu component, a parent
component that rendered the Recipe component. We want to take a
moment to look at a small example of rendering two sibling
components using a React fragment. Let’s start by creating a new
component called Cat that we’ll render to the DOM at the root:
This will render the h1 as expected, but what might happen if we added
a p tag to the Cat component at the same level as the h1?
Immediately, we’ll see an error in the console that reads Adjacent JSX
elements must be wrapped in an enclosing tag and
recommends using a fragment. This is where fragments come into
play! React will not render two or more adjacent or sibling elements as
a component, so we used to have to wrap these in an enclosing tag like
a div. This led to a lot of unnecessary tags being created, though, and a
bunch of wrappers without much purpose. If we use a React fragment,
we can mimic the behavior of a wrapper without actually creating a
new tag.
Adding this clears the warning. You also can use a fragment shorthand
to make this look even cleaner:
If you look at the DOM, the fragment is not visible in the resulting tree:
<div id="root">
<h1>The cat's name is Jungle</h1>
<p>He's good</p>
</div>
Fragments are a relatively new feature of React and do away with the
need for extra wrapper tags that can pollute the DOM.
Intro to webpack
Once we start working with React in real projects, there are a lot of
questions to consider: How do we want to deal with JSX and ESNext
transformation? How can we manage our dependencies? How can we
optimize our images and CSS?
Modularity will allow you to break down your source code into parts,
or modules, that are easier to work with, especially in a team
environment.
Network performance is gained by only needing to load one
dependency in the browser: the bundle. Each script tag makes an
HTTP request, and there’s a latency penalty for each HTTP request.
Bundling all the dependencies into a single file allows you to load
everything with one HTTP request, thereby avoiding additional
latency.
Code splitting
Splits up your code into different chunks that can be loaded when
you need them. Sometimes these are called rollups or layers; the
aim is to break up code as needed for different pages or devices.
Minification
Removes whitespace, line breaks, lengthy variable names, and
unnecessary code to reduce the file size.
Feature Flagging
Sends code to one or more—but not all—environments when
testing out features.
The Recipes app we built earlier in this chapter has some limitations
that webpack will help us alleviate. Using a tool like webpack to
statically build client JavaScript makes it possible for teams to work
together on large-scale web applications. We can also gain the
following benefits by incorporating the webpack module bundler:
Modularity
Using the module pattern to export modules that will later be
imported or required by another part of the application makes
source code more approachable. It allows development teams to
work together, by allowing them to create and work with separate
files that will be statically combined into a single file before
sending to production.
Composition
With modules, we can build small, simple, reusable React
components that we can compose efficiently into applications.
Smaller components are easier to comprehend, test, and reuse.
They’re also easier to replace down the line when enhancing
applications.
Speed
Packaging all the application’s modules and dependencies into a
single client bundle will reduce the load time of an application
because there’s latency associated with each HTTP request.
Packaging everything together in a single file means that the client
will only need to make a single request. Minifying the code in the
bundle will improve load time as well.
Consistency
Since webpack will compile JSX and JavaScript, we can start using
tomorrow’s JavaScript syntax today. Babel supports a wide range
of ESNext syntax, which means we don’t have to worry about
whether the browser supports our code. It allows developers to
consistently use cutting-edge JavaScript syntax.
Creating the Project
To demonstrate how we might set up a React project from scratch, let’s
go ahead and create a new folder on our computer called recipes-app:
mkdir recipes-app
cd recipes-app
CREATE-REACT-APP
There’s a tool called Create React App that can be used to autogenerate a React
project with all of this preconfigured. We’re going to take a closer look at what’s
happening behind the scenes before abstracting these steps away with a tool.
Next, we’ll create the project and package.json file with npm, sending
the -y flag to use all of the defaults. We’ll also install webpack,
webpack-cli, react, and react-dom:
npm init -y
npm install react react-dom serve
If we’re using npm 5, we don’t need to send the --save flag when
installing. Next, we’ll create the following directory structure to house
the components:
recipes-app (folder)
> node_modules (already added with npm install command)
> package.json (already added with npm init command)
> package-lock.json (already added with npm init command)
> index.html
> /src (folder)
> index.js
> /data (folder)
> recipes.json
> /components (folder)
> Recipe.js
> Instructions.js
> Ingredients.js
FILE ORGANIZATION
There’s no one way to organize the files in a React project. This is just one
possible strategy.
// ./src/components/Recipe.js
// ./src/components/Instructions.js
Now think about the ingredients. In the Recipe component, we’re only
displaying the ingredient names, but each ingredient in the data for the
recipe has an amount and measurement as well. We’ll create a
component called Ingredient for this:
// ./src/components/Ingredient.js
// ./src/components/IngredientsList.js
<Ingredient
amount={ingredient.amount}
measurement={ingredient.measurement}
name={ingredient.name}
/>
let ingredient = {
amount: 1,
measurement: "cup",
name: "sugar"
};
We get:
// ./src/components/Recipe.js
// ./src/components/Menu.js
// ./src/index.js
import React from "react";
import { render } from "react-dom";
import Menu from "./components/Menu";
import data from "./data/recipes.json";
The first four statements import the necessary modules for our app to
work. Instead of loading react and react-dom via the script tag, we
import them so webpack can add them to our bundle. We also need the
Menu component and a sample data array that has been moved to a
separate module. It still contains two recipes: Baked Salmon and Fish
Tacos.
All of our imported variables are local to the index.js file. When we
render the Menu component, we pass the array of recipe data to this
component as a property.
The data is being pulled from the recipes.json file. This is the same
data we used earlier in the chapter, but now it’s following valid JSON
formatting rules:
// ./src/data/recipes.json
[
{
"name": "Baked Salmon",
"ingredients": [
{ "name": "Salmon", "amount": 1, "measurement": "lb" },
{ "name": "Pine Nuts", "amount": 1, "measurement": "cup" },
{ "name": "Butter Lettuce", "amount": 2, "measurement": "cups" },
{ "name": "Yellow Squash", "amount": 1, "measurement": "med" },
{ "name": "Olive Oil", "amount": 0.5, "measurement": "cup" },
{ "name": "Garlic", "amount": 3, "measurement": "cloves" }
],
"steps": [
"Preheat the oven to 350 degrees.",
"Spread the olive oil around a glass baking dish.",
"Add the yellow squash and place in the oven for 30 mins.",
"Add the salmon, garlic, and pine nuts to the dish.",
"Bake for 15 minutes.",
"Remove from oven. Add the lettuce and serve."
]
},
{
"name": "Fish Tacos",
"ingredients": [
{ "name": "Whitefish", "amount": 1, "measurement": "lb" },
{ "name": "Cheese", "amount": 1, "measurement": "cup" },
{ "name": "Iceberg Lettuce", "amount": 2, "measurement": "cups" },
{ "name": "Tomatoes", "amount": 2, "measurement": "large" },
{ "name": "Tortillas", "amount": 3, "measurement": "med" }
],
"steps": [
"Cook the fish on the grill until cooked through.",
"Place the fish on the 3 tortillas.",
"Top them with lettuce, tomatoes, and cheese."
]
}
]
Now that we’ve pulled our code apart into separate modules and files,
let’s create a build process with webpack that will put everything back
together into a single file. You may be thinking, “Wait, we just did all
of that work to break everything apart, and now we’re going to use a
tool to put it back together? Why on Earth…?” Splitting projects into
separate files typically makes larger projects easier to manage because
team members can work on separate components without overlap. It
also means that files can be easier to test.
For this modular Recipes app to work, we’re going to need to tell
webpack how to bundle our source code into a single file. As of
version 4.0.0, webpack does not require a configuration file to bundle a
project. If we don’t include a config file, webpack will run the defaults
to package our code. Using a config file, though, means we’ll be able
to customize our setup. Plus, this shows us some of the magic of
webpack instead of hiding it away. The default webpack configuration
file is always webpack.config.js.
The starting file for our Recipes app is index.js. It imports React,
ReactDOM, and the Menu.js file. This is what we want to run in the
browser first. Wherever webpack finds an import statement, it will
find the associated module in the filesystem and include it in the
bundle. index.js imports Menu.js, Menu.js imports Recipe.js, Recipe.js
imports Instructions.js and IngredientsList.js, and IngredientsList.js
imports Ingredient.js. Webpack will follow this import tree and include
all of these necessary modules in our bundle. Traversal through all
these files creates what’s called a dependency graph. A dependency is
just something our app needs, like a component file, a library file like
React, or an image. Picture each file we need as a circle on the graph,
with webpack drawing all the lines between the circles to create the
graph. That graph is the bundle.
IMPORT STATEMENTS
We’re using import statements, which are not presently supported by most
browsers or by Node.js. The reason import statements work is that Babel will
convert them into require('module/path'); statements in our final code. The
require function is how CommonJS modules are typically loaded.
// ./webpack.config.js
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "dist", "assets"),
filename: "bundle.js"
}
};
First, we tell webpack that our client entry file is ./src/index.js. It will
automatically build the dependency graph based on import statements
starting in that file. Next, we specify that we want to output a bundled
JavaScript file to ./dist/bundle.js. This is where webpack will place the
final packaged JavaScript.
module.exports = {
entry: "./src/index.js",
output: {
path: path.join(__dirname, "dist", "assets"),
filename: "bundle.js"
},
module: {
rules: [{ test: /\.js$/, exclude: /node_modules/, loader: "babel-
loader" }]
}
};
The rules field is an array because there are many types of loaders
you can incorporate with webpack. In this example, we’re only
incorporating the babel-loader. Each loader is a JavaScript object.
The test field is a regular expression that matches the file path of each
module that the loader should operate on. In this case, we’re running
the babel-loader on all imported JavaScript files except those found
in the node_modules folder.
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
Webpack is run statically. Typically, bundles are created before the app
is deployed to the server. You can run it from the command line using
npx:
Webpack will either succeed and create a bundle or fail and show you
an error. Most errors have to do with broken import references. When
debugging webpack errors, look closely at the filenames and file paths
used in import statements.
You can also add an npm script to your package.json file to create a
shortcut:
"scripts": {
"build": "webpack --mode production"
},
// ./dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React Recipes App</title>
</head>
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>
</html>
This is the home page for your app. It will load everything it needs
from one file, one HTTP request: bundle.js. You’ll need to deploy
these files to your web server or build a web server application that will
serve these files with something like Node.js or Ruby on Rails.
Source Mapping
Bundling our code into a single file can cause some setbacks when it
comes time to debug the application in the browser. We can eliminate
this problem by providing a source map. A source map is a file that
maps a bundle to the original source files. With webpack, all we have
to do is add a couple lines to our webpack.config.js file.
The source map is going to let you debug using your original source
files. In the Sources tab of your browser’s developer tools, you should
find a folder named webpack://. Inside of this folder, you’ll see all the
source files in your bundle, as shown in Figure 5-4.
Figure 5-4. Sources panel of Chrome Developer Tools
You can debug from these files using the browser step-through
debugger. Clicking on any line number adds a breakpoint. Refreshing
the browser will pause JavaScript processing when any breakpoints are
reached in your source file. You can inspect scoped variables in the
Scope panel or add variables to watch in the Watch panel.
To get started with Create React App, install the package globally:
Then, use the command and the name of the folder where you’d like
the app to be created:
create-react-app my-project
NPX
You can also use npx to run Create React App without the need for a global
install. Simply run npx create-react-app my-project.
This will create a React project in that directory with just three
dependencies: React, ReactDOM, and react-scripts. react-
scripts was also created by Facebook and is where the real magic
happens. It installs Babel, ESLint, webpack, and more so that you don’t
have to configure them manually. Within the generated project folder,
you’ll also find a src folder containing an App.js file. Here, you can
edit the root component and import other component files.
From within the my-react-project folder, you can run npm start. If
you prefer, you can also run yarn start. This will start your
application on port 3000.
You can run tests with npm test or yarn test. This runs all of the
test files in the project in an interactive mode.
You can also run the npm run build command. Using yarn, run yarn
build.
Create React App is a great tool for beginners and experienced React
developers alike. As the tool evolves, more functionality will likely be
added, so you can keep an eye on the changes on GitHub. Another way
to get started with React without having to worry about setting up your
own customized webpack build is to use CodeSandbox. CodeSandbox
is an IDE that runs online at https://codesandbox.io.
Data is what makes our React components come to life. The user
interface for recipes that we built in the last chapter is useless without
the array of recipes. It’s the recipes and the ingredients along with clear
instructions that makes such an app worthwhile. Our user interfaces are
tools that creators will use to generate content. In order to build the
best tools possible for our content creators, we’ll need to know how to
effectively manipulate and change data.
The Star component renders an individual star and uses the selected
property to fill it with the appropriate color. If the selected property is
not passed to this component, we’ll assume that the star should not be
selected and by default will be filled in with grey.
The 5-star rating system is pretty popular, but a 10-star rating system is
far more detailed. We should allow developers to select the total
number of stars they wish to use when they add this component to their
app. This can be accomplished by adding a totalStars property to the
StarRating component:
The stars the user has selected represents the rating. We’ll create a state
variable called selectedStars, which will hold the user’s rating.
We’ll create this variable by adding the useState hook directly to the
StarRating component:
In order to collect a different rating from the user, we’ll need to allow
them to click on any of our stars. This means we’ll need to make the
stars clickable by adding an onClick handler to the FaStar
component:
Now that our Star component is clickable, we’ll use it to change the
state of the StarRating:
The most important thing to remember about Hooks is that they can
cause the component they’re hooked into to rerender. Every time we
invoke the setSelectedStars function to change the value of
selectedStars, the StarRating function component will be
reinvoked by the hook, and it will render again, this time with a new
value for selectedStars. This is why Hooks are such a killer feature.
When data within the hook changes, they have the power to rerender
the component they’re hooked into with new data.
The React developer tools will show you which Hooks are
incorporated with specific components. When we render the
StarRating component in the browser, we can view debugging
information about that component by selecting it in the developer tools.
In the column on the right, we can see that the StarRating component
incorporates a state Hook that has a value of 2. As we interact with the
app, we can watch the state value change and the component tree
rerender with the corresponding number of stars selected.
The following code is a class component. This was the original StarRating component that was
printed in the first edition of this book:
change(starsSelected) {
this.setState({ starsSelected });
}
render() {
const { totalStars } = this.props;
const { starsSelected } = this.state;
return (
<div>
{[...Array(totalStars)].map((n, i) => (
<Star
key={i}
selected={i < starsSelected}
onClick={() => this.change(i + 1)}
/>
))}
<p>
{starsSelected} of {totalStars} stars
</p>
</div>
);
}
}
This class component does the same thing as our function component with noticeably more code.
Additionally, it introduces more confusion thorough the use of the this keyword and function binding.
As of today, this code still works. We’re no longer covering class components in this book because
they’re no longer needed. Function components and Hooks are the future of React, and we’re not
looking back. There could come a day where class components are officially deprecated, and this
code will no longer be supported.
First, let’s consider the style property. This property allows you to
add CSS styles to elements. It is highly possible that a future
developer, even yourself, could come across the need to modify the
style of your entire container. They may attempt to do something like
this:
All we need to do is collect those styles and pass them down to the
StarRating container. Currently, the StarRating does not have a
single container because we are using a React fragment. To make this
work, we’ll have to upgrade from a fragment to a div element and pass
the styles to that div:
In the code above, we replaced the fragment with a div element and
then applied styles to that div element. By default we assign that div a
padding of 5px, and then we use the spread operator to apply the rest of
the properties from the style object to the div style.
The first step is to collect any and all properties that the user may be
attempting to add to the StarRating. We gather these properties using
the spread operator: ...props. Next, we’ll pass all of these remaining
properties down to the div element: {...props}.
Let’s build a small application that can be used to save a list of colors.
We’ll call the app the “Color Organizer”, and it will allow users to
associate a list of colors with a custom title and rating. To get started, a
sample dataset may look like the following:
[
{
"id": "0175d1f0-a8c6-41bf-8d02-df5734d829a4",
"title": "ocean at dusk",
"color": "#00c4e2",
"rating": 5
},
{
"id": "83c7ba2f-7392-4d7d-9e23-35adbe186046",
"title": "lawn",
"color": "#26ac56",
"rating": 3
},
{
"id": "a11e3995-b0bd-4d58-8c48-5e49ae7f7f23",
"title": "bright red",
"color": "#ff0000",
"rating": 0
}
]
The ColorList receives the colors from the App component as props.
If the list is empty, this component will display a message to our users.
When we have a color array, we can map over it and pass the details
about each color farther down the tree to the Color component:
NOTE
It is possible for the StarRating component to hold its own state and receive
state from a parent component via props. This is typically necessary when
distributing components for wider use by the community. We demonstrate this
technique in the next chapter when we cover the useEffect hook.
At this point, we’ve finished passing state down the component tree
from the App component all the way to each Star component that’s
filled red to visually represent the rating for each color. If we render
the app based on the color-data.json file that was listed previously, we
should see our colors in the browser, as shown in Figure 6-5.
Figure 6-5. Color Organizer rendered in the browser
For instance, let’s say we wanted to add a Remove button next to each
color’s title that would allow users to remove colors from state. We
would add that button to the Color component:
export default function Color({ id, title, color, rating, onRemove = f =>
f }) {
return (
<section>
<h1>{title}</h1>
<button onClick={() => onRemove(id)}>
<FaTrash />
</button>
<div style={{ height: 50, backgroundColor: color }} />
<StarRating selectedStars={rating} />
</section>
);
}
Here, we’ve modified the color by adding a button that will allow users
to remove colors. First, we imported a trash can icon from react-
icons. Next, we wrapped the FaTrash icon in a button. Adding an
onClick handler to this button allows us to invoke the onRemove
function property, which has been added to our list of properties along
with the id. When a user clicks the Remove button, we’ll invoke
removeColor and pass it the id of the color that we want to remove.
That is why the id value has also been gathered from the Color
component’s properties.
return (
colors.map(color => (
<Color key={color.id} {...color} onRemove={onRemoveColor} />
)
}
</div>
);
}
Changing the state of the colors array causes the App component to be
rerendered with the new list of colors. Those new colors are passed to
the ColorList component, which is also rerendered. It will render
Color components for the remaining colors and our UI will reflect the
changes we’ve made by rendering one less color.
If we want to rate the colors that are stored in the App components
state, we’ll have to repeat the process with an onRate event. First,
we’ll collect the new rating from the individual star that was clicked
and pass that value to the parent of the StarRating:
Then, we’ll grab the rating from the onRate handler we added to the
StarRating component. We’ll then pass the new rating along with the
id of the color to be rated up to the Color component’s parent via
another onRate function property:
The App component will change color ratings when the ColorList
invokes the onRateColor property with the id of the color to rate and
the new rating. We’ll use those values to construct an array of new
colors by mapping over the existing colors and changing the rating for
the color that matches the id property. Once we send the newColors to
the setColors function, the state value for colors will change and the
App component will be invoked with a new value for the colors array.
Once the state of our colors array changes, the UI tree is rendered
with the new data. The new rating is reflected back to the user via red
stars. Just as we passed data down a component tree via props,
interactions can be passed back up the tree along with data via function
properties.
Building Forms
For a lot of us, being a web developer means collecting large amounts
of information from users with forms. If this sounds like your job, then
you’ll be building a lot of form components with React. All of the
HTML form elements that are available to the DOM are also available
as React elements, which means that you may already know how to
render a form with JSX:
<form>
<input type="text" placeholder="color title..." required />
<input type="color" required />
<button>ADD</button>
</form>
This form element has three child elements: two input elements and a
button. The first input element is a text input that will be used to
collect the title value for new colors. The second input element is an
HTML color input that will allow users to pick a color from a color
wheel. We’ll be using basic HTML form validation, so we’ve marked
both inputs as required. The ADD button will be used to add a new
color.
Using Refs
When it’s time to build a form component in React, there are several
patterns available to you. One of these patterns involves accessing the
DOM node directly using a React feature called refs. In React, a ref is
an object that stores values for the lifetime of a component. There are
several use cases that involve using refs. In this section, we’ll look at
how we can access a DOM node directly with a ref.
React provides us with a useRef hook that we can use to create a ref.
We’ll use this hook when building the AddColorForm component:
return (...)
}
First, when creating this component, we’ll also create two refs using
the useRef hook. The txtTitle ref will be used to reference the text
input we’ve added to the form to collect the color title. The hexColor
ref will be used to access hexadecimal color values from the HTML
color input. We can set the values for these refs directly in JSX using
the ref property:
return (
<form onSubmit={submit}>
<input ref={txtTitle} type="text" placeholder="color title..."
required />
<input ref={hexColor} type="color" required />
<button>ADD</button>
</form>
);
}
Here, we set the value for the txtTitle and hexColor refs by adding
the ref attribute to these input elements in JSX. This creates a current
field on our ref object that references the DOM element directly. This
provides us access to the DOM element, which means we can capture
its value. When the user submits this form by clicking the ADD button,
we’ll invoke the submit function:
Next, we capture the current values for each of our form elements
using their refs. These values are then passed up to this component’s
parent via the onNewColor function property. Both the title and the
hexadecimal value for the new color are passed as function arguments.
Finally, we reset the value attribute for both inputs to clear the data
and prepare the form to collect another color.
Did you notice the subtle paradigm shift that has occurred by using
refs? We’re mutating the value attribute of DOM nodes directly by
setting them equal to "" empty strings. This is imperative code. The
AddColorForm is now what we call an uncontrolled component
because it uses the DOM to save the form values. Sometimes using
uncontrolled component can get you out of problems. For instance, you
may want to share access to a form and its values with code outside of
React. However, a controlled component is a better approach.
Controlled Components
In a controlled component, the from values are managed by React and
not the DOM. They do not require us to use refs. They do not require
us to write imperative code. Adding features like robust form
validation is much easier when working with a controlled component.
Let’s modify the AddColorForm by giving it control over the form’s
state:
return ( ... );
}
First, instead of using refs, we’re going to save the values for the title
and color using React state. We’ll create variables for title and
color. Additionally, we’ll define the functions that can be used to
change state: setTitle and setColor.
Now that the component controls the values for title and color, we
can display them inside of the form input elements by setting the value
attribute. Once we set the value attribute of an input element, we’ll no
longer be able to change with the form. The only way to change the
value at this point would be to change the state variable every time the
user types a new character in the input element. That’s exactly what
we’ll do:
<form onSubmit={submit}>
<input
value={title}
onChange={event => setTitle(event.target.value)}
type="text"
placeholder="color title..."
required
/>
<input
value={color}
onChange={event => setColor(event.target.value)}
type="color"
required
/>
<button>ADD</button>
</form>
}
This controlled component now sets the value of both input elements
using the title and color from state. Whenever these elements raise
an onChange event, we can access the new value using the event
argument. The event.target is a reference to the DOM element, so
we can obtain the current value of that element with
event.target.value. When the title changes, we’ll invoke
setTitle to change the title value in state. Changing that value will
cause this component to rerender, and we can now display the new
value for title inside the input element. Changing the color works
exactly the same way.
When it’s time to submit the form, we can simply pass the state values
for title and color to the onNewColor function property as
arguments when we invoke it. The setTitle and setColor functions
can be used to reset the values after the new color has been passed to
the parent component:
value={title}
onChange={event => setTitle(event.target.value)}
It might seem like you’re working faster by simply copying and pasting
these properties into every form element while tweaking the variable
names along the way. However, whenever you copy and paste code,
you should hear a tiny little alarm sound in your head. Copying and
pasting code suggests that there’s something redundant enough to
abstract away in a function.
This is a custom hook. It doesn’t take a lot of code. Inside of this hook,
we’re still using the useState hook to create a state value. Next, we
return an array. The first value of the array is the object that contains
the same properties we were tempted to copy and paste: the value
from state along with an onChange function property that changes that
value in state. The second value in the array is a function that can be
reused to reset the value back to its initial value. We can use our hook
inside of the AddColorForm:
return ( ... )
}
return (
<form onSubmit={submit}>
<input
{...titleProps}
type="text"
placeholder="color title..."
required
/>
<input {...colorProps} type="color" required />
<button>ADD</button>
</form>
);
}
Spreading these properties from our custom hook is much more fun
than pasting them. Now both the title and the color inputs are receiving
properties for their value and onChange events. We’ve used our hook
to create controlled form inputs without worrying about the underlying
implementation details. The only other change we need to make is
when this form is submitted:
Within the submit function, we need to be sure to grab the value for
both the title and the color from their properties. Finally, we can use
the custom reset functions that were returned from the useInput hook.
Let’s add the AddColorForm, whichever one you choose, to the the App
component. When the onNewColor property is invoked, we’ll save the
new color in state:
With this change, we’ve completed the first iteration of the Color
Organizer. Users can now add new colors to the list, remove colors
from the list, and rate any existing color on that list.
React Context
Storing state in one location at the root of our tree was an important
pattern that helped us all be more successful with early versions of
React. Learning to pass state both down and up a component tree via
properties is a necessary right of passage for any React developer—it’s
something we should all know how to do. However, as React evolved
and our component trees got larger, following this principle slowly
became more unrealistic. It’s hard for many developers to maintain
state in a single location at the root of a component tree for a complex
application. Passing state down and up the tree through dozens of
components is tedious and bug ridden.
The UI elements that most of us work on are complex. The root of the
tree is often very far from the leaves. This puts data the application
depends on many layers away from the components that use the data.
Every component must receive props that they only pass to their
children. This will bloat our code and make our UI harder to scale.
It’s obviously more efficient to fly from San Francisco to DC. This
way, you don’t have to pass through every state—you simply fly over
them (Figure 6-7).
Figure 6-7. Flight from San Francisco to DC
In React, context is like jet-setting for your data. You can place data in
React context by creating a context provider. A context provider is a
React component you can wrap around your entire component tree or
specific sections of your component tree. The context provider is the
departing airport where your data boards the plane. It’s also the airline
hub. All flights depart from that airport to different destinations. Each
destination is a context consumer. The context consumer is the React
component that retrieves the data from context. This is the destination
airport where your data lands, deplanes, and goes to work.
Using context still allows to us store state data in a single location, but
it doesn’t require us to pass that data through a bunch of components
that don’t need it.
Let’s place the default colors found in the color-data.json file into
context. We’ll add context to the index.js file, the entry point of our
application:
render(
<ColorContext.Provider value={{ colors }}>
<App />
</ColorContext.Provider>,
document.getElementById("root")
);
NOTE
A context Provider doesn’t always have to wrap an entire application. It’s not
only OK to wrap specific sections components with a context Provider, it can
make your application more efficient. The Provider will only provide context
values to its children.
It’s OK to use multiple context providers. In fact, you may be using context
providers in your React app already without even knowing it. Many npm packages
designed to work with React use context behind the scenes.
Now that we’re providing the colors value in context, the App
component no longer needs to hold state and pass it down to its
children as props. We’ve made the App component a “flyover”
component. The Provider is the App component’s parent, and it’s
providing the colors in context. The ColorList is the App
component’s child, and it can obtain the colors directly on its own. So
the app doesn’t need to touch the colors at all, which is great because
the App component itself has nothing to do with colors. That
responsibility has been delegated farther down the tree.
We can remove a lot of lines of code from the App component. It only
needs to render the AddColorForm and the ColorList. It no longer has
to worry about the data:
The stateful component that renders the context provider is our custom
provider. That is: that’s the component that will be used when it’s time
to wrap our App with the provider. In a brand-new file, let’s create a
ColorProvider:
You may have noticed that the setColors function is also being added
to context. This gives context consumers the ability to change the value
for colors. Whenever setColors is invoked, the colors array will
change. This will cause the ColorProvider to rerender, and our UI
will update itself to display the new colors array.
Adding setColors to context may not be the best idea. It invites other
developers and you to make mistakes later on down the road when
using it. There are only three options when it comes to changing the
value of the colors array: users can add colors, remove colors, or rate
colors. It’s a better idea to add functions for each of these operations to
context. This way, you don’t expose the setColors function to
consumers; you only expose functions for the changes they’re allowed
to make:
return (
<ColorContext.Provider value={{ colors, addColor, removeColor,
rateColor }}>
{children}
</ColorContext.Provider>
);
};
render(
<ColorProvider>
<App />
</ColorProvider>,
document.getElementById("root")
);
Now, any component that’s a child of the App can obtain the colors
from the useColors hook. The ColorList component needs to access
the colors array to render the colors on the screen:
return ( ... );
}
Introducing useEffect
We now have a good sense of what happens when we render a
component. A component is simply a function that renders a user
interface. Renders occur when the app first loads and when props and
state values change. But what happens when we need to do something
after a render? Let’s take a closer look.
function Checkbox() {
const [checked, setChecked] = useState(false);
alert(`checked: ${checked.toString()}`);
return (
<>
<input
type="checkbox"
value={checked}
onChange={() => setChecked(checked => !checked)}
/>
{checked ? "checked" : "not checked"}
</>
);
};
We’ve added the alert before the render to block the render. The
component will not render until the user clicks the OK button on the
alert box. Because the alert is blocking, we don’t see the next state of
the checkbox rendered until clicking OK.
That isn’t the goal, so maybe we should place the alert after the return?
function Checkbox {
const [checked, setChecked] = useState(false);
return (
<>
<input
type="checkbox"
value={checked}
onChange={() => setChecked(checked => !checked)}
/>
{checked ? "checked" : "not checked"}
</>
);
alert(`checked: ${checked.toString()}`);
};
Scratch that. We can’t call alert after the render because the code will
never be reached. To ensure that we see the alert as expected, we can
use useEffect. Placing the alert inside of the useEffect function
means that the function will be called after the render, as a side effect:
function Checkbox {
const [checked, setChecked] = useState(false);
useEffect(() => {
alert(`checked: ${checked.toString()}`);
});
return (
<>
<input
type="checkbox"
value={checked}
onChange={() => setChecked(checked => !checked)}
/>
{checked ? "checked" : "not checked"}
</>
);
};
We use useEffect when a render needs to cause side effects. Think of
a side effect as something that a function does that isn’t part of the
return. The function is the Checkbox. The Checkbox function renders
UI. But we might want the component to do more than that. Those
things we want the component to do other than return UI are called
effects.
useEffect(() => {
console.log(checked ? "Yes, checked" : "No, not checked");
});
useEffect(() => {
localStorage.setItem("checkbox-value", checked);
});
We might also use useEffect to focus on a specific text input that has
been added to the DOM. React will render the output, then call
useEffect to focus the element:
useEffect(() => {
txtInputRef.current.focus();
});
On render, the txtInputRef will have a value. We can access that
value in the effect to apply the focus. Every time we render,
useEffect has access to the latest values from that render: props, state,
refs, etc.
Consider the following, where the App component has two separate
state values:
function App() {
const [val, set] = useState("");
const [phrase, setPhrase] = useState("example phrase");
useEffect(() => {
console.log(`saved phrase: "${phrase}"`);
});
return (
<>
<label>Favorite phrase:</label>
<input
value={val}
placeholder={phrase}
onChange={e => set(e.target.value)}
/>
<button onClick={createPhrase}>send</button>
</>
);
}
val is a state variable that represents the value of the input field. The
val changes every time the value of the input field changes. It causes
the component to render every time the user types a new character.
When the user clicks the Send button, the val of the text area is saved
as the phrase, and the val is reset to "", which empties the text field.
useEffect(() => {
console.log(`typing "${val}"`);
}, [val]);
useEffect(() => {
console.log(`saved phrase: "${phrase}"`);
}, [phrase]);
Changing the val value by typing into the input only causes the first
effect to fire. When we click the button, the phrase is saved and the
val is reset to "".
It’s an array after all, so it’s possible to check multiple values in the
dependency array. Let’s say we wanted to run a specific effect any time
either the val or phrase has changed:
useEffect(() => {
console.log("either val or phrase has changed");
}, [val, phrase]);
If either of those values changes, the effect will be called again. It’s
also possible to supply an empty array as the second argument to a
useEffect function. An empty dependency array causes the effect to
be invoked only once after the initial render:
useEffect(() => {
console.log("only once after initial render");
}, []);
Since there are no dependencies in the array, the effect is invoked for
the initial render. No dependencies means no changes, so the effect will
never be invoked again. Effects that are only invoked on the first
render are extremely useful for initialization:
useEffect(() => {
welcomeChime.play();
}, []);
If you return a function from the effect, the function will be invoked
when the component is removed from the tree:
useEffect(() => {
welcomeChime.play();
return () => goodbyeChime.play();
}, []);
This means that you can use useEffect for setup and teardown. The
empty array means that the welcome chime will play once on first
render. Then, we’ll return a function as a cleanup function to play a
goodbye chime when the component is removed from the tree.
useEffect(() => {
newsFeed.subscribe(addPost);
welcomeChime.play();
return () => {
newsFeed.unsubscribe(addPost);
goodbyeChime.play();
};
}, []);
useEffect(() => {
newsFeed.subscribe(addPost);
return () => newsFeed.unsubscribe(addPost);
}, []);
useEffect(() => {
welcomeChime.play();
return () => goodbyeChime.play();
}, []);
useEffect(() => {
newsFeed.subscribe(addPost);
return () => newsFeed.unsubscribe(addPost);
}, []);
useEffect(() => {
welcomeChime.play();
return () => goodbyeChime.play();
}, []);
return posts;
};
Our custom hook contains all of the functionality to handle a jazzy
news feed, which means that we can easily share this functionality with
our components. In a new component called NewsFeed, we’ll use the
custom hook:
return (
<>
<h1>{posts.length} articles</h1>
{posts.map(post => (
<Post key={post.id} {...post} />
))}
</>
);
}
In JavaScript, arrays, objects, and functions are the same only when
they’re the exact same instance. So how does this relate to the
useEffect dependency array? To demonstrate this, we’re going to
need a component we can force to render as much as we want. Let’s
build a hook that causes a component to render whenever a key is
pressed:
useEffect(() => {
window.addEventListener("keydown", forceRender);
return () => window.removeEventListener("keydown", forceRender);
}, []);
};
With the custom hook built, we can use it in the App component (and
any other component for that matter! Hooks are cool.):
function App() {
useAnyKeyToRender();
useEffect(() => {
console.log("fresh render");
});
Declaring words outside of the scope of the App would solve the
problem:
function App() {
useAnyKeyToRender();
useEffect(() => {
console.log("fresh render");
}, [words]);
return <h1>component</h1>;
}
The dependency array in this case refers to one instance of words that’s
declared outside of the function. The “fresh render” effect does not get
called again after the first render because words is the same instance as
the last render. This is a good solution for this example, but it’s not
always possible (or advisable) to have a variable defined outside of the
scope of the function. Sometimes the value passed to the dependency
array requires variables in scope. For example, we might need to create
the words array from a React property like children:
useEffect(() => {
console.log("fresh render");
}, [words]);
return (
<>
<p>{children}</p>
<p>
<strong>{words.length} - words</strong>
</p>
</>
);
}
function App() {
return <WordCount>You are not going to believe this but...</WordCount>;
}
The App component contains some words that are children of the
WordCount component. The WordCount component takes in children
as a property. Then we set words in the component equal to an array of
those words that we’ve called .split on. We would hope that the
component will rerender only if words changes, but as soon as we
press a key, we see the dreaded “fresh render” words appearing in the
console.
Let’s replace that feeling of dread with one of calm, because the React
team has provided us a way to avoid these extra renders. They
wouldn’t hang us out to dry like that. The solution to this problem is, as
you might expect, another hook: useMemo.
useEffect(() => {
console.log("fresh render");
}, [words]);
useMemo invokes the function sent to it and sets words to the return
value of that function. Like useEffect, useMemo relies on a
dependency array:
const words = useMemo(() => children.split(" "));
When we don’t include the dependency array with useMemo, the words
are calculated with every render. The dependency array controls when
the callback function should be invoked. The second argument sent to
the useMemo function is the dependency array and should contain the
children value:
useEffect(() => {
console.log("fresh render");
}, [words]);
return (...);
}
const fn = () => {
console.log("hello");
console.log("world");
};
useEffect(() => {
console.log("fresh render");
fn();
}, [fn]);
useEffect(() => {
console.log("fresh render");
fn();
}, [fn]);
useCallback memoizes the function value for fn. Just like useMemo
and useEffect, it also expects a dependency array as the second
argument. In this case, we create the memoized callback once because
the dependency array is empty.
useEffect(() => {
newPostChime.play();
}, [posts]);
useEffect(() => {
newsFeed.subscribe(addPost);
return () => newsFeed.unsubscribe(addPost);
}, []);
useEffect(() => {
welcomeChime.play();
return () => goodbyeChime.play();
}, []);
return posts;
};
Now, the useJazzyNews hook plays a chime every time there’s a new
post. We made this happen with a few changes to the hook. First,
const [posts, setPosts] was renamed to const [_posts,
setPosts]. We’ll calculate a new value for posts every time _posts
change.
Next, we added the effect that plays the chime every time the post
array changes. We’re listening to the news feed for new posts. When a
new post is added, this hook is reinvoked with _posts reflecting that
new post. Then, a new value for post is memoized because _posts
have changed. Then the chime plays because this effect is dependent
on posts. It only plays when the posts change, and the list of posts
only changes when a new one is added.
When to useLayoutEffect
We understand that the render always comes before useEffect. The
render happens first, then all effects run in order with full access to all
of the values from the render. A quick look at the React docs will point
out that there’s another type of effect hook: useLayoutEffect.
1. Render
2. useLayoutEffect is called
3. Browser paint: the time when the component’s elements are
actually added to the DOM
4. useEffect is called
function App() {
useEffect(() => console.log("useEffect"));
useLayoutEffect(() => console.log("useLayoutEffect"));
return <div>ready</div>;
}
useLayoutEffect
useEffect
function useWindowSize {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
window.addEventListener("resize", resize);
resize();
return () => window.removeEventListener("resize", resize);
}, []);
function useMousePosition {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useLayoutEffect(() => {
window.addEventListener("mousemove", setPosition);
return () => window.removeEventListener("mousemove", setPosition);
}, []);
It’s highly likely that the x and y position of the mouse will be used
when painting the screen. useLayoutEffect is available to help
calculate those positions accurately before the paint.
function Counter() {
useEffect(() => {
...
}, [checked]);
useEffect(() => {
...
}, []);
useEffect(() => {
...
}, [count]);
return ( ... )
}
The order of Hook calls is the same for each and every render:
function Counter() {
if (count > 5) {
useEffect(() => {
...
});
if (count > 5) {
useEffect(() => {
...
});
}
useEffect(() => {
...
});
return ( ... )
}
function Counter() {
useState(
? undefined
: !c,
useEffect(() => {
...
});
useEffect(() => {
...
});
useEffect(() => {
...
});
return ( ... )
}
Here, the value for checked is based on the condition that the
count is greater than 5. When count is less than 5, the value for
checked is undefined. Nesting this conditional inside the hook
means that the hook remains on the top level, but the result is
similar. The second effect enforces the same rules. If the count is
less than 5, the return statement will prevent the effect from
continuing to execute. This keeps the hook values array intact:
[countValue, checkedValue, DependencyArray,
DependencyArray, DependencyArray].
Like conditional logic, you need to nest asynchronous behavior
inside of a hook. useEffect takes a function as the first argument,
not a promise. So you can’t use an async function as the first
argument: useEffect(async () => {}). You can, however,
create an async function inside of the nested function like this:
useEffect(() => {
await SomePromise();
};
fn();
});
useEffect(() => {
(async () => {
await SomePromise();
})();
});
If you follow these rules, you can avoid some common gotchas with
React Hooks. If you’re using Create React App, there’s an ESLint
plug-in included called eslint-plugin-react-hooks that provides warning
hints if you’re in violation of these rules.
function Checkbox() {
const [checked, setChecked] = useState(false);
return (
<>
<input
type="checkbox"
value={checked}
onChange={() => setChecked(checked => !checked)}
/>
{checked ? "checked" : "not checked"}
</>
);
}
This works well, but one area of this function could be cause for alarm:
Let’s add a function called toggle that will do the same thing: call
setChecked and return the opposite of the current value of checked:
function Checkbox() {
const [checked, setChecked] = useState(false);
function toggle() {
setChecked(checked => !checked);
}
return (
<>
<input type="checkbox" value={checked} onChange={toggle} />
{checked ? "checked" : "not checked"}
</>
);
}
function Checkbox() {
const [checked, toggle] = useReducer(checked => !checked, false);
return (
<>
<input type="checkbox" value={checked} onChange={toggle} />
{checked ? "checked" : "not checked"}
</>
);
}
useReducer takes in the reducer function and the initial state, false.
Then, we’ll set the onChange function to setChecked, which will call
the reducer function.
function Numbers() {
const [number, setNumber] = useReducer(
(number, newNumber) => number + newNumber,
0
);
return <h1 onClick={() => setNumber(30)}>{number}</h1>;
}
const firstUser = {
id: "0391-3233-3201",
firstName: "Bill",
lastName: "Wilson",
city: "Missoula",
state: "Montana",
email: "bwilson@mtnwilsons.com",
admin: false
};
Then we have a component called User that sets the firstUser as the
initial state, and the component displays the appropriate data:
function User() {
const [user, setUser] = useState(firstUser);
return (
<div>
<h1>
{user.firstName} {user.lastName} - {user.admin ? "Admin" :
"User"}
</h1>
<p>Email: {user.email}</p>
<p>
Location: {user.city}, {user.state}
</p>
<button>Make Admin</button>
</div>
);
}
<button
onClick={() => {
setUser({ admin: true });
}}
>
Make Admin
</button>
Doing this would overwrite state from firstUser and replace it with
just what we sent to the setUser function: {admin: true}. This can
be fixed by spreading the current values from user, then overwriting
the admin value:
<button
onClick={() => {
setUser({ ...user, admin: true });
}}
>
Make Admin
</button>
This will take the initial state and push in the new key/values: {admin:
true}. We need to rewrite this logic in every onClick, making it prone
to error (we might forget to do this when we come back to the app
tomorrow):
function User() {
const [user, setUser] = useReducer(
(user, newDetails) => ({ ...user, ...newDetails }),
firstUser
);
...
}
Then we’ll send the new state value, newDetails, to the reducer, and it
will be pushed into the object:
<button
onClick={() => {
setUser({ admin: true });
}}
>
Make Admin
</button>
This pattern is useful when state has multiple subvalues or when the
next state depends on a previous state. Teach everyone to spread,
they’ll spread for a day. Teach everyone to useReducer and they’ll
spread for life.
The older incarnation of setState merged state values. The same is true of useReducer:
If you like this pattern, you can use legacy-set-state npm or useReducer.
The past few examples are simple applications for a reducer. In the next chapter, we’ll dig deeper into
reducer design patterns that can be used to simplify state management in your apps.
function App() {
const [cats, setCats] = useState(["Biscuit", "Jungle", "Outlaw"]);
return (
<>
{cats.map((name, i) => (
<Cat key={i} name={name} />
))}
<button onClick={() => setCats([...cats, prompt("Name a cat")])}>
Add a Cat
</button>
</>
);
}
This app uses the Cat component. After the initial render, the console
reads:
rendering Biscuit
rendering Jungle
rendering Outlaw
When the “Add a Cat” button is clicked, the user is prompted to add a
cat.
If we add a cat named “Ripple,” we see that all Cat components are
rerendered:
rendering Biscuit
rendering Jungle
rendering Outlaw
rendering Ripple
WARNING
This code works because prompt is blocking. This is just an example. Don’t use
prompt in a real app.
Every time we add a cat, every Cat component is rendered, but the Cat
component is a pure component. Nothing changes about the output
given the same prop, so there shouldn’t be a render for each of these.
We don’t want to rerender a pure component if the properties haven’t
changed. The memo function can be used to create a component that
will only render when its properties change. Start by importing it from
the React library and use it to wrap the current Cat component:
Now, every time we add a new cat name, like “Pancake,” we see only
one render in the console:
rendering Pancake
Because the names of the other cats have not changed, we don’t render
those Cat components. This is working well for a name property, but
what if we introduce a function property to the Cat component?
Every time a cat is clicked on, we can use this property to log a meow to
the console:
The memo function will allow us to define more specific rules around
when this component should rerender:
const RenderCatOnce = memo(Cat, () => true);
const AlwaysRenderCat = memo(Cat, () => false);
When to Refactor
The last Hooks we discussed, useMemo and useCallback, along with
the memo function, are commonly overused. React is designed to be
fast. It’s designed to have components render a lot. The process of
optimizing for performance began when you decided to use React in
the first place. It’s fast. Any further refactoring should be a last step.
Always make sure your app works and you’re satisfied with the
codebase before refactoring. Over-refactoring, or refactoring before
your app works, can introduce weird bugs that are hard to spot, and it
might not be worth your time and focus to introduce these
optimizations.
In the last two chapters, we’ve introduced many of the Hooks that ship
with React. You’ve seen use cases for each hook, and you’ve created
your own custom Hooks by composing other Hooks. Next, we’ll build
on these foundational skills by incorporating additional libraries and
advanced patterns.
Chapter 8. Incorporating Data
When we talk about data, it may sound a little like we’re talking about
water or food. The cloud is the abundantly endless source from which
we send and receive data. It’s the internet. It’s the networks, services,
systems, and databases where we manipulate and store zettabytes of
data. The cloud hydrates our clients with the latest and freshest data
from the source. We work with this data locally and even store it
locally. But when our local data becomes out of sync with the source, it
loses its freshness and is said to be stale.
Requesting Data
In the movie Star Wars, the droid C-3P0 is a protocol droid. His
specialty, of course, is communication. He speaks over six million
languages. Surely, C-3P0 knows how to send an HTTP request,
because the Hyper Text Transfer Protocol is one of the most popular
ways to transmit data to and from the internet.
fetch(`https://api.github.com/users/moonhighway`)
.then(response => response.json())
.then(console.log)
.catch(console.error);
{
"login": "MoonHighway",
"id": 5952087,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjU5NTIwODc=",
"avatar_url": "https://avatars0.githubusercontent.com/u/5952087?v=4",
"bio": "Web Development classroom training materials.",
...
Typically, we use a POST request when we’re creating data and a PUT
request when we’re modifying it. The second argument of the fetch
function allows us to pass an object of options that fetch can use when
creating our HTTP request:
fetch("/create/user", {
method: "POST",
body: JSON.stringify({ username, password, bio })
});
This fetch is using the POST method to create a new user. The
username, password, and user’s bio are being passed as string content
in the body of the request.
fetch("/create/user", {
method: "POST",
body: formData
});
Authorized Requests
Sometimes, we need to be authorized to make requests. Authorization
is typically required to obtain personal or sensitive data. Additionally,
authorization is almost always required for users to take action on the
server with POST, PUT, or DELETE requests.
useEffect(() => {
if (!login) return;
fetch(`https://api.github.com/users/${login}`)
.then(response => response.json())
.then(setData)
.catch(console.error);
}, [login]);
if (data)
return <pre>{JSON.stringify(data, null, 2)}</pre>;
return null;
}
We could save the user’s data that we received from our GitHub
request. Then the next time that same user is requested, we could use
the data saved to localStorage instead of sending another request to
GitHub. We’ll add the following code to the GitHubUser component:
When data changes here after it has been loaded from GitHub, we’ll
invoke saveJSON to save only those user details that we need: name,
login, avatar_url, and location. No need to save the rest of the
user object when we’re not using it. We also skip saving the data
when that object is empty, !data. Also, if the current login and
data.login are equal to each other, then we already have saved data
for that user. We’ll skip the step of saving that data again.
useEffect(() => {
if (!data) return;
if (data.login === login) return;
const { name, avatar_url, location } = data;
saveJSON(`user:${login}`, {
name,
login,
avatar_url,
location
});
}, [data]);
useEffect(() => {
if (!login) return;
if (data && data.login === login) return;
fetch(`https://api.github.com/users/${login}`)
.then(response => response.json())
.then(setData)
.catch(console.error);
}, [login]);
if (data)
return <pre>{JSON.stringify(data, null, 2)}</pre>;
return null;
}
{
"login": "MoonHighway",
"id": 5952087,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjU5NTIwODc=",
"avatar_url": "https://avatars0.githubusercontent.com/u/5952087?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/MoonHighway",
"html_url": "https://github.com/MoonHighway",
...
This is the response from GitHub. We can tell because this object
contains a lot of extra information about the user that we don’t need.
The first time we run this page we’ll see this lengthy response. But the
second time we run the page, the response is much shorter:
{
"name": "Moon Highway",
"login": "moonhighway",
"avatar_url": "https://avatars0.githubusercontent.com/u/5952087?v=4",
"location": "Tahoe City, CA"
}
This time, the data we saved locally for moonhighway is being rendered
to the browser. Since we only needed four fields of data, we only saved
four fields of data. We’ll always see this smaller offline object until we
clear the storage:
localStorage.clear();
We really need to handle all three of these states when we make HTTP
requests. We can modify the GitHub user component to render more
than just a successful response. We can add a “loading…” message
when the request is pending, or we can render the error details if
something goes wrong:
useEffect(() => {
if (!login) return;
setLoading(true);
fetch(`https://api.github.com/users/${login}`)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [login]);
return (
<div className="githubUser">
<img
src={data.avatar_url}
alt={data.login}
style={{ width: 200 }}
/>
<div>
<h1>{data.login}</h1>
{data.name && <p>{data.name}</p>}
{data.location && <p>{data.location}</p>}
</div>
</div>
);
}
Handling all three of these states bloats our code a little bit, but it’s
essential to do so on every request. Requests take time and a lot could
go wrong. Because all requests—and promises—have these three
states, it makes it possible to handle all HTTP requests with a reusable
hook, or a component, or even a React feature called Suspense. We’ll
cover each of these approaches, but first, we must introduce the
concept of render props.
Render Props
Render props are exactly what they sound like: properties that are
rendered. This can mean components that are sent as properties that are
rendered when specific conditions are met, or it can mean function
properties that return components that will be rendered. In the second
case, when they’re functions, data can be passed as arguments and used
when rendering the returned component.
const tahoe_peaks = [
{ name: "Freel Peak", elevation: 10891 },
{ name: "Monument Peak", elevation: 10067 },
{ name: "Pyramid Peak", elevation: 9983 },
{ name: "Mt. Tallac", elevation: 9735 }
];
In this example, the four tallest peaks in Tahoe are rendered into an
unordered list. This code makes sense, but mapping over an array to
render each item individually does introduce some code complexity.
Mapping over an array of items is also a pretty common task. We may
find ourselves frequently repeating this pattern. We could create a List
component that we can reuse as a solution whenever we need to render
an unordered list.
In this case, users would see the following message: This list is
empty.
Doing so at this point only renders the number of items found within
the array: 4 items.
We can also tell our List component what to render for each item
found within the array. For example, we can send a renderItem
property:
This time, the render prop is a function. The data (the item itself) is
passed to this function as an argument so that it can be used when what
to render for each Tahoe Peak is decided. In this case, we render a
React fragment that displays the item’s name and elevation. If the
array is tahoe_peaks, we expect the renderItem property to be
invoked four times: once for each of the peaks in the array.
When the data array is not empty, the List component renders an
unordered list, <ul>. It maps over each item within the array using the
.map method and renders a list item, <li>, for every value within the
array. The List component makes sure each list item receives a unique
key. Within each <li> element, the renderItem property is invoked
and the item itself is passed to that function property as an argument.
The result is an unordered list that displays the name and elevation of
each of Tahoe’s tallest peaks.
The good news is we have a reusable List component that we can use
whenever we need to render an unordered list. The bad news is our
component is a bit bare bones. There are better components we can use
to handle this task.
Virtualized Lists
If it’s our job to develop a reusable component for rendering lists, there
are many different use cases to consider and solutions to implement.
One of the most important things to consider is what happens when the
list is very large. Many of the data points we work with in production
can feel infinite. A Google search yields pages and pages of results.
Searching for a place to stay in Tahoe on Airbnb results in a list of
houses and apartments that seems to never end. Production applications
typically have a lot of data that needs to be rendered, but we can’t
render it all at once.
There’s a limit to what the browser can render. Rendering takes time,
processing power, and memory, all three of which have eventual
limitations. This should be taken into consideration when developing a
reusable list component. When the data array is very large, what
should we do?
Even though our search for a place to stay may have yielded one
thousand results, we cannot possibly look at all those results at the
same time—there’s not enough screen space for all the images, names,
and prices. We might only be able to see about five results at a time.
When scrolling, we can see more results, but we have to scroll down
pretty far to see a thousand results. Rendering a thousand results in a
scrollable layer is asking a lot of the phone.
As the user scrolls, we can unmount the results that have already been
viewed as well as render new results off screen, ready for the user to
reveal via the scroll. This resulting solution means that the browser will
only render 11 elements at a time while the data for the rest of the
elements is there waiting to be rendered. This technique is called
windowing or virtualization. It allows us to scroll very large,
sometimes infinite lists of data without crashing our browser.
npm i faker
Installing faker will allow us to create a large array of fake data. For
this example, we’ll use fake users. We’ll create five thousand fake
users at random:
This code creates a div element for each user. Within that div, an img
element is rendered for that user’s photo, and the user name and email
are rendered with a paragraph element, as shown in Figure 8-3.
Figure 8-3. Performance results
npm i react-window
react-window is a library that has several components we can use to
render virtualized lists. In this example, we’ll use the FixSizeList
component from react-window:
return (
<FixedSizeList
height={window.innerHeight}
width={window.innerWidth - 20}
itemCount={bigList.length}
itemSize={50}
>
{renderRow}
</FixedSizeList>
);
}
FixedSizeList is slightly different from our List component. It
requires the total number of items in the list along with the number of
pixels each row requires as the itemSize property. Another big
difference in this syntax is that the render prop is being passed to
FixedSizeList as the children property. This render props pattern is
used quite frequently.
So, let’s see what happens when five thousand fake users are rendered
with the FixSizeList component (see Figure 8-4).
This time, not all of the users are being rendered at once. Only those
rows that the user can see or easily scroll to are being rendered. Notice
that it only takes 2.6ms for this initial render.
useEffect(() => {
if (!uri) return;
fetch(uri)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [uri]);
return {
loading,
data,
error
};
}
All three of these states are managed inside of the useEffect hook.
This hook is invoked every time the value for uri changes. If there’s
no uri, the fetch request is not made. When there’s a uri, the fetch
request begins. If the request is successful, we pass the resulting JSON
to the setData function, changing the state value for data. After that,
we then change the state value for loading to false because the request
was successful (i.e., it’s no longer pending). Finally, if anything goes
wrong, we catch it and pass it to setError, which changes the state
value for error.
Now we can use this hook to make fetch requests within our
components. Anytime the values for loading, data, or error change,
this hook causes the GitHubUser component to rerender with those
new values:
return (
<div className="githubUser">
<img
src={data.avatar_url}
alt={data.login}
style={{ width: 200 }}
/>
<div>
<h1>{data.login}</h1>
{data.name && <p>{data.name}</p>}
{data.location && <p>{data.location}</p>}
</div>
</div>
);
}
Although the component now has less logic, it still handles all three
states. Assuming we have a SearchForm component ready to collect
search strings from the user, we can add the GitHubUser component to
our main App component:
return (
<>
<SearchForm value={login} onSearch={setLogin} />
<GitHubUser login={login} />
</>
);
}
The main App component stores the username of the GitHub user in
state. The only way to change this value is to use the search form to
search for a new user. Whenever the value of login changes, the value
sent to useFetch changes because it depends on the login property:
https://api.github.com/users/${login}. This changes the uri within our
hook and triggers a fetch request for the new user login. We’ve created
a custom hook and used it to successfully create a small application
that can be used to look up and display GitHub user details. We’ll
continue to use this hook as we iterate on this application.
function Fetch({
uri,
renderSuccess,
loadingFallback = <p>loading...</p>,
renderError = error => (
<pre>{JSON.stringify(error, null, 2)}</pre>
)
}) {
const { loading, data, error } = useFetch(uri);
if (loading) return loadingFallback;
if (error) return renderError(error);
if (data) return renderSuccess({ data });
}
With the Fetch component in our arsenal, we can really simplify the
logic in our GitHubUser component:
<Fetch
uri={`https://api.github.com/users/${login}`}
loadingFallback={<LoadingSpinner />}
renderError={error => {
// handle error
return <p>Something went wrong... {error.message}</p>;
}}
renderSuccess={({ data }) => (
<>
<h1>Todo: Render UI for data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</>
)}
/>
This time, the Fetch component will render our custom loading
spinner. If something goes wrong, we hide the error details. When the
request is successful, we’ve chosen to alternatively render the raw data
along with a TODO message for ourselves.
This hook will allow us to cycle through any array. Because it returns
items inside of an array, we can take advantage of array destructuring
to give these values names that make sense:
In this case, the initial letter is “b.” If the user invokes next, the
component will rerender, but this time, the value for letter will be
“b.” Invoke next two more times, and the value for letter will once
again be “a” because this iterator circles back around to the first item in
the array instead of letting the index go out of bounds.
Memozing Values
The useIterator hook is pretty cool. But we can do even better by
memoizing the value for item as well as the function for prev and
next:
Here, both prev and next are created with the useCallback hook.
This ensures that the function for prev will always be the same until
the value for i changes. Likewise, the item value will always point to
the same item object unless the value for i changes.
If they click the Next button, they’ll see the name of the next
repository. Likewise, if they click the Previous button, they’ll see the
name of the previous repository. RepoMenu is the component we’ll
create to provide this feature:
useEffect(() => {
if (!name) return;
onSelect(name);
}, [name]);
return (
<div style={{ display: "flex" }}>
<button onClick={previous}><</button>
<p>{name}</p>
<button onClick={next}>></button>
</div>
);
}
In order to get information about Eve’s account along with her list of
repositories, we need to send two separate HTTP requests. A majority
of our lives as React developers will be spent like this: making multiple
requests for information and composing all of the information received
into beautiful user interface applications. Making two requests for
information is just the beginning. In the next section, we’ll continue to
make more requests of GitHub so we can see the README.md for the
selected repository.
Waterfall Requests
In the last section, we made two HTTP requests. The first request was
for a user’s details, then once we had those details, we made a second
request for that user’s repositories. These requests happen one at a
time, one after the other.
The first request is made when we initially fetch the user’s details:
<Fetch
uri={`https://api.github.com/users/${login}`}
renderSuccess={UserDetails}
/>
<Fetch
uri={`https://api.github.com/users/${login}/repos`}
renderSuccess={({ data }) => (
<RepoMenu repositories={data} onSelect={onSelect} />
)}
/>
We call these requests waterfall requests because they happen one right
after the other—they’re dependent on each other. If something goes
wrong with the user details request, the request for that user’s
repositories is never made.
Let’s add some more layers (water?) to this waterfall. First, we request
the user’s info, then their repository list, then, once we have their
repository list, we make a request for the first repository’s
README.md file. As the user cycles through the list of repositories,
we’ll make additional requests for the associated README to each
repository.
Repository README files are written using Markdown, which is a
text format that can be easily rendered as HTML with the
ReactMarkdown component. First, let’s install react-markdown:
npm i react-markdown
import React, {
useState,
useEffect,
useCallback
} from "react";
import ReactMarkdown from "react-markdown";
useEffect(() => {
if (!repo || !login) return;
loadReadme(login, repo).catch(setError);
}, [repo]);
if (error)
return <pre>{JSON.stringify(error, null, 2)}</pre>;
if (loading) return <p>Loading...</p>;
Notice we have to handle the same three render states that we do for
every fetch request: pending, success, and fail. Finally, when we have a
successful response, the Markdown itself is rendered using the
ReactMarkdown component.
The Network tab is available under the developer tools of most major
browsers. To throttle the network speed in Google Chrome, select the
arrow next to the word “Online,” as demonstrated in Figure 8-6.
This will open a menu where you can choose various speeds, as you
can see in Figure 8-7.
Additionally, the Network tab displays a timeline for all of the HTTP
requests. You can filter this timeline to only view “XHR” requests.
This means it will only show the request made using fetch (Figure 8-
8).
Here, we see that four requests were made one after the other. Notice
that the loading graphic is titled “Waterfall.” This shows that each
request is made after the other is complete.
Parallel Requests
Sometimes, it’s possible to make an application faster by sending all
requests at once. Instead of having each request occur one after another
in a waterfall, we can send our requests in parallel, or at the same time.
If the user searches for another GitHubUser with the SearchForm, the
value for login will change, which will trigger the useEffect hooks
within our components, causing them to make additional requests for
data. If the user cycles through the list of repositories, then the
onSelect property for UserRepositories will be invoked, which
causes the repo value to change. Changing the repo value will trigger
the useEffect hook inside of the RepositoryReadme component, and
a new README will be requested.
The second argument for the useIterator hook is the initial index to
start with. If there’s a selected property, then we’ll search for the
index of the selected repository by name. This is required to make sure
the repository menu displays the correct repository initially. We also
need to pass this selected property to this component from
UserRepositories:
<Fetch
uri={`https://api.github.com/users/${login}/repos`}
renderSuccess={({ data }) => (
<RepoMenu
repositories={data}
selected={repo}
onSelect={onSelect}
/>
)}
/>
Now that the repo property is being passed down to the RepoMenu, the
menu should select the initial repository, which in our case is
“learning-react.”
If you take a look at the Network tab, you’ll notice we’ve made three
requests in parallel, as shown in Figure 8-9.
Canceling Requests
Thinking about our application a little bit more, we realize that the user
could empty the search field and search for no user at all. In this case,
we would also want to make sure that the value for repo is also empty.
Let’s add a handleSearch method that makes sure the repo value
changes when there’s no value for login:
if (!login)
return (
<SearchForm value={login} onSearch={handleSearch} />
);
return (
<>
<SearchForm value={login} onSearch={handleSearch} />
<GitHubUser login={login} />
<UserRepositories
login={login}
repo={repo}
onSelect={setRepo}
/>
<RepositoryReadme login={login} repo={repo} />
</>
);
}
We’ve added a handleSearch method. Now, when the user clears the
search field and searches for an empty string, the repo value is also set
to an empty string. If for some reason there’s not a login, we only
render one component: the SearchForm. When we have a value for
login, we’ll render all four components.
Now, technically our app has two screens. One screen only displays a
search form. The other screen only shows when the search form
contains a value, in which case, it shows all four components. We’ve
set ourselves up to mount or unmount components based on user
interactivity. Let’s say we were looking at the details for
“moonhighway.” If the user empties the search field, then the
GitHubUser, UserRepositories, and RepositoryReadme
components are unmounted and will no longer be displayed. But what
if these components were in the middle of loading data when they were
unmounted?
Whenever our users load data over a slow network, these errors can
occur. But we can protect ourselves. First, we can create a hook that
will tell us whether or not the current component is mounted:
Now we can use this hook inside of the RepoReadme component. This
will allow us to make sure the component is mounted before applying
any state updates:
useEffect(() => {
if (!uri) return;
if (!mounted.current) return;
setLoading(true);
fetch(uri)
.then(data => {
if (!mounted.current) throw new Error("component is not mounted");
return data;
})
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(error => {
if (!mounted.current) return;
setError(error);
});
The useFetch hook is used to make the rest of the fetch requests in our
app. In this hook, we compose the fetch request using thenables,
chainable .then() functions, instead of async/await. When the fetch
is complete, we check to see if the component is mounted in the first
.then callback. If the component is mounted, the data is returned and
the rest of the .then functions are invoked. When the component is not
mounted, the first .then function throws an error, preventing the rest
of the .then functions from executing. Instead, the .catch function is
invoked and the new error is passed to that function. The .catch
function will check to see if the component is mounted before it tries to
invoke setError.
The left panel of the Explorer is where we draft our GraphQL query.
Inside of this panel, we could add a query to obtain information about a
single GitHub user:
query {
user(login: "moontahoe") {
id
login
name
location
avatarUrl
}
}
{
"data": {
"user": {
"id": "MDQ6VXNlcjU5NTIwODI=",
"login": "MoonTahoe",
"name": "Alex Banks",
"location": "Tahoe City, CA",
"avatarUrl": "https://github.com/moontahoe.png"
}
}
}
In addition to obtaining details about a user, we’re also asking for that
user’s first hundred repositories. We’re asking for the number of
repositories returned by the query, the totalCount, along with the
name of each repository. GraphQL only returns the data we ask for. In
this case, we’ll only get the name for each repository, nothing else.
There’s one more change that we made to this query: we used an alias
for the avatarUrl. The GraphQL field to obtain a user’s avatar is
called avatarUrl, but we want that variable to be named avatar_url.
The alias tells GitHub to rename that field in the data response.
NOTE
GraphQL is not restricted to HTTP. It’s a specification of how data requests
should be made over a network. It can technically work with any network
protocol. Additionally, GraphQL is language-agnostic.
First, let’s install graphql-request:
npm i graphql-request
user
public_repo
repo
repo_deployment
repo:status
read:repo_hook
read:org
read:public_key
read:gpg_key
client
.request(query, { login: "moontahoe" })
.then(results => JSON.stringify(results, null, 2))
.then(console.log)
.catch(console.error);
{
"user": {
"id": "MDQ6VXNlcjU5NTIwODI=",
"login": "MoonTahoe",
"name": "Alex Banks",
"location": "Tahoe City, CA",
"avatar_url": "https://avatars0.githubusercontent.com/u/5952082?v=4",
"repositories": {
"totalCount": 52,
"nodes": [
{
"name": "snowtooth"
},
{
"name": "Memory"
},
{
"name": "snowtooth-status"
},
...
]
}
}
}
Just like fetch, client.request returns a promise. Getting this data
inside of your React component will feel very similar to fetching data
from a route:
return (
<>
<SearchForm value={login} onSearch={setLogin} />
<UserDetails {...userData} />
<p>{userData.repositories.totalCount} - repos</p>
<List
data={userData.repositories.nodes}
renderItem={repo => <span>{repo.name}</span>}
/>
</>
);
}
This example doesn’t put care into handling loading and error states,
but we can apply everything we learned in the rest of this chapter to
GraphQL. React doesn’t really care how we get the data. As long as we
understand how to work with asynchronous objects like promises
within our components, we’ll be ready for anything.
useEffect(() => {
socket.on("connection", () => setStatus("connected"));
socket.on("disconnecting", () =>
setStatus("disconnected")
);
socket.on("message", setStatus);
return () => {
socket.removeAllListeners("connect");
socket.removeAllListeners("disconnect");
socket.removeAllListeners("message");
};
}, []);
return {
status,
messages,
send
};
}
This is the least important chapter in this book. At least, that’s what
we’ve been told by the React team. They didn’t specifically say, “this
is the least important chapter, don’t write it.” They’ve only issued a
series of tweets warning educators and evangelists that much of their
work in this area will very soon be outdated. All of this will change.
It could be said that the work the React team has done with Fiber,
Suspense, and concurrent mode represents the future of web
development. This work may change the way browsers interpret
JavaScript. That sounds pretty important. We’re saying that this is the
least important chapter in this book because the community hype for
Suspense is high; we need to say it to balance out your expectations.
The APIs and patterns that make up Suspense are not the single
overarching theory that defines how all things large and small should
operate.
Suspense is a just a feature. You may not ever need to use it. It’s being
designed to solve specific problems that Facebook experiences
working at scale. We don’t all have the same problems as Facebook, so
we may want to think twice before reaching for those tools as the
solution to all our problems. They may unnecessarily introduce
complexity where complexity is not needed. Plus, this is all going to
change. Concurrent mode is an experimental feature, and the React
team has issued stern warnings about trying to use it in production. In
fact, most of these concepts involve using hooks. If you don’t see
yourself developing custom hooks on a daily basis, you’ll probably
never need to know about these features. Much of the mechanics
involving Suspense can be abstracted away in hooks.
This component will be used to give our layout some style, as shown in
Figure 9-1.
Error Boundaries
Thus far, we haven’t done the best job with handling errors. An error
thrown anywhere in our component tree will take down the entire
application. Larger component trees only further complicate our project
and complicate debugging it. Sometimes, it can be hard to pinpoint
where an error has occurred, especially when they occur within
components that we didn’t write.
static getDerivedStateFromError(error) {
return { error };
}
render() {
const { error } = this.state;
const { children, fallback } = this.props;
Now we can use this component in our tree to capture errors and render
a fallback component if they occur. For example, we could wrap our
entire application with an error boundary:
return (
<div className="error">
<h3>We are sorry... something went wrong</h3>
<p>We cannot process your request at this moment.</p>
<p>ERROR: {error.message}</p>
</div>
);
}
<ErrorBoundary fallback={ErrorScreen}>
<App />
</ErrorBoundary>;
The ErrorScreen provides a gentle message for our users that an error
has occurred. It renders some details about the error. It also gives us a
place to potentially track errors that occur anywhere within our app. If
an error does occur within the app, this component will be rendered
instead of a black screen. We can make this component look nice with
a little CSS:
.error {
background-color: #efacac;
border: double 4px darkred;
color: darkred;
padding: 1em;
}
return (
<SiteLayout
menu={
<ErrorBoundary fallback={ErrorScreen}>
<p>Site Layout Menu</p>
<BreakThings />
</ErrorBoundary>
}
>
<ErrorBoundary fallback={ErrorScreen}>
<Callout>Callout<BreakThings /></Callout>
</ErrorBoundary>
<ErrorBoundary fallback={ErrorScreen}>
<h1>Contents</h1>
<p>this is the main part of the example layout</p>
</ErrorBoundary>
</SiteLayout>
Now we see the menu and the callout being rendered without issue, but
the contents has rendered an error to notify the user that an error has
occurred.
render() {
const { error } = this.state;
const { children } = this.props;
return children;
}
<ErrorBoundary>
<h1><Contents /></h1>
<p>this is the main part of the example layout</p>
<BreakThings />
</ErrorBoundary>
Code Splitting
If the applications you’re working on are small now, chances are they
won’t stay that way. A lot of the applications you work on will
eventually contain massive codebases with hundreds, maybe even
thousands, of components. Most of your users could be accessing your
applications via their phones on potentially slow networks. They can’t
wait for the entire codebase of your application to successfully
download before React completes its first render.
Next, we’ll move the rest of our codebase from a component called App
to a component called Main, and we’ll place that component in its own
file:
So Main is where the current site layout is rendered. Now we’ll modify
the App component to render the Agreement until the user agrees to it.
When they agree, we’ll unmount the Agreement component and render
the Main website component:
if (!agree)
return <Agreement onAgree={() => setAgree(true)} />;
We’re telling React to wait to load the codebase for the Main
component until it’s initially rendered. When it is rendered, it will be
imported at that time using the import function.
Importing code during runtime is just like loading anything else from
the internet. First, the request for the JavaScript code is pending. Then
it’s either successful, and a JavaScript file is returned, or it fails,
causing an error to occur. Just like we need to notify a user that we’re
in the process of loading data, we’ll need to let the user know that
we’re in the process of loading code.
We can modify the app to lazy load the Main component with the
following code:
if (!agree)
return <Agreement onAgree={() => setAgree(true)} />;
return (
<Suspense fallback={<ClimbingBoxLoader />}>
<Main />
</Suspense>
);
}
Now the app initially only loads the codebase for React, the
Agreement component, and the ClimbingBoxLoader. React will hold
off on loading the Main component until the user agrees to the
agreement.
NOTE
React Spinners is a library of animated loading spinners that indicate that
something is loading or that the app is working. For the remainder of this chapter,
we’ll be sampling different loader components from this library. Make sure you
install this library: npm i react-spinners.
What happens when the internet connection goes down before trying to
load the Main component? Well, we’ll have an error on our hands. We
can handle that by wrapping our Suspense component within an
ErrorBoundary:
<ErrorBoundary fallback={ErrorScreen}>
<Suspense fallback={<ClimbingBoxLoader />}>
<Main />
</Suspense>
</ErrorBoundary>
function Status() {
const status = loadStatus();
return <h1>status: {status}</h1>;
}
If we were to run this code as-is, we would see our successful status
message, as shown in Figure 9-4.
Figure 9-4. Success: everything works
Now when we run our application, we see the expected output. The
ErrorBoundary caught our error and rendered a message to the user
instead (Figure 9-5).
Figure 9-5. Fail: error boundary triggered
We have two of the three states covered. What about the third state?
Pending? That state can be triggered by throwing a promise:
Throwing Promises
In JavaScript, the throw keyword is technically for errors. You’ve
probably used it many times in your own code:
Unhandled errors are always visible in the console. All the red text we
see in the console is information about the error we’ve thrown.
Here, we’ve thrown a string. The browser will tell us that something
has gone uncaught, but it’s not an error (Figure 9-9).
This time, when we threw a string, the Create React App error screen
wasn’t rendered inside the browser. React knows the difference
between an error and a string.
function safe(fn) {
try {
fn();
} catch (error) {
if (error instanceof Promise) {
error.then(() => safe(fn));
} else {
throw error;
}
}
}
function Status() {
const status = loadStatus();
return <h1>status: {status}</h1>;
}
We see the same pattern as we did with the safe function. The
Suspense component knows that a promise was thrown. It will render
the fallback component. Then the Suspense component waits for the
thrown promise to be resolved, just like the safe function did. Once
resolved, the Suspense component rerenders the Status component.
When Status renders again, it calls loadStatus and the whole
process repeats itself. We see “load status” printed to the console,
every three seconds, endlessly, forever.
An endless loop is typically not the desired output. It isn’t for React,
either. It’s important to know that, when we throw a promise, it’s
caught by the Suspense component, and we enter into a pending state
until the promise has been resolved.
Building Suspenseful Data Sources
A Suspenseful data source needs to provide a function that handles all
the states associated with loading data: pending, success, and error. The
loadStatus function can only return or throw one type at a time. We
need the loadStatus function to throw a promise when the data is
loading, return a response when the data is successful, or throw an
error if something goes wrong:
function loadStatus() {
if (error) throw error;
if (response) return response;
throw promise;
}
return function() {
if (error) throw error;
if (response) return response;
throw promise;
};
})();
This is a closure. The scope of the error, promise, and response are
closed off from any code outside of the function where they’re defined.
When we declare loadStatus, an anonymous function is declared and
immediately invoked: fn() is the same as (fn)(). The value of
loadStatus becomes the inner function that’s returned. The
loadStatus function now has access to error, promise, and
response, but the rest of our JavaScript world does not.
Now all we need to do is handle the values for error, response, and
promise. The promise will be pending for three seconds before it’s
successfully resolved. When the promise resolves, the value for
response will be set to “success.” We’ll catch any errors or promise
rejections and use them to set the error value:
function createResource(pending) {
let error, response;
pending.then(r => (response = r)).catch(e => (error = e));
return {
read() {
if (error) throw error;
if (response) return response;
throw pending;
}
};
}
This function still closes off the values for error and response, but it
allows consumers to pass in a promise as an argument called pending.
When the pending promise is resolved, we capture the results with a
.then function. If the promise is rejected, we’ll catch the error and use
it to assign a value to the error variable.
function Gnar() {
const result = resource.read();
return <h1>Gnar: {result.gnar}</h1>;
}
When the promise has resolved, the Suspense component will attempt
to render Gnar again. Gnar will invoke resource.read() again, but
this time, assuming everything went OK, resource.read() will
successfully return Gnar, which is used to render the state of Gnar in an
h1 element. If something went wrong, resource.read() would have
thrown an error, which would be handed by the ErrorBoundary.
At present, this is how Suspense works. This is how we can use the
Suspense component with any type of asynchronous resource. This
could all change, and we expect it to change. However, whatever the
finalized API for Suspense ends up being, it will be sure to handle
three states: pending, success, and fail.
The look at these Suspense APIs has been kind of high-level, and this
was intentional because this stuff is experimental. It’s going to change.
What’s important to take away from this chapter is that React is always
tinkering with ways to make React apps faster.
Behind the scenes of a lot of this work is the way that React itself
works—specifically, its reconciliation algorithm called Fiber.
Fiber
Throughout this book, we’ve talked about React components as being
functions that return data as a UI. Every time this data changes (props,
state, remote data, etc), we rely on React to rerender the component. If
we click a star to rate a color, we assume that our UI will change, and
we assume that it’ll happen fast. We assume this because we trust
React to make it happen. How exactly does this work though? To
understand how React efficiently updates the DOM, let’s take a closer
look at how React works.
Consider that you’re writing an article for your company blog. You
want feedback, so you send the article to your coworker before you
publish. They recommend a few quick changes, and now you need to
incorporate those changes. You create a brand-new document, type out
the entire article from scratch, and then add in the edits.
Now, you’re writing another blog post and you send it to your
coworker again. This time, you’ve modernized your article-writing
process to use GitHub. Your coworker checks out a GitHub branch,
makes the changes, and merges in the branch when they’re finished.
Faster and more efficient.
<ul>
<li>blue</li>
<li>purple</li>
<li>red</li>
</ul>
React would not get rid of the third li. Instead it would replace its
children (red text) with green text. This is an efficient approach to
updating and is the way that React has updated the DOM since its
inception. There is a potential problem here, though. Updating the
DOM is an expensive task because it’s synchronous. We have to wait
for all of the updates to be reconciled and then rendered before we can
do other tasks on the main thread. In other words, we’d have to wait for
React to recursively move through all of the updates, which could
make the user experience seem unresponsive.
Separating the renderer from the reconciler was a big deal. The
reconciliation algorithm was kept in React Core (the package you
install to use React), and each rendering target was made responsible
for rendering. In other words, ReactDOM, React Native, React 360,
and more would be responsible for the logic of rendering and could be
plugged into React’s core reconciliation algorithm.
Another huge shift with React Fiber was its changes to the
reconciliation algorithm. Remember our expensive DOM updates that
blocked the main thread? This lengthy block of updates is called work
—with Fiber, React split the work into smaller units of work called
fibers. A fiber is a JavaScript object that keeps track of what it’s
reconciling and where it is in the updating cycle.
Once a fiber (unit of work) is complete, React checks in with the main
thread to make sure there’s not anything important to do. If there is
important work to do, React will give control to the main thread. When
it’s done with that important work, React will continue its update. If
there’s nothing critical to jump to on the main thread, React moves on
to the next unit of work and renders those changes to the DOM.
If this was all Fiber did, it would be a success, but there’s even more to
it than that! In addition to the performance benefits of breaking work
into smaller units, the rewrite also sets up exciting possibilities for the
future. Fiber provides the infrastructure for prioritizing updates. In the
longer term, the developer may even be able to tweak the defaults and
decide which types of tasks should be given the highest priority. The
process of prioritizing units of work is called scheduling; this concept
underlies the experimental concurrent mode, which will eventually
allow these units of work to be performed in parallel.
ESLint
In most programming languages, code needs to be compiled before you
can run anything. Programming languages have pretty strict rules about
coding style and will not compile until the code is formatted
appropriately. JavaScript does not have those rules and does not come
with a compiler. We write code, cross our fingers, and run it in the
browser to see if it works or not. The good news is that there are tools
we can use to analyze our code and make us stick to specific formatting
guidelines.
ESLint is supported out of the box with Create React App, and we’ve
already seen lint warnings and errors appear in the console.
# or
Where does your code run? (Press space to select, a to toggle all,
i to invert selection)
Browser
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": ["react"],
"rules": {}
}
Importantly, if we look at the extends key, we’ll see that our --init
command initalized defaults for eslint and react. This means that we
don’t have to manually configure all of the rules. Instead, those rules
are provided to us.
const info = ({
file = __filename,
dir = __dirname
}) => (
<p>
{dir}: {file}
</p>
);
switch (gnar) {
default:
console.log("gnarly");
break;
}
This file has some issues, but nothing that would cause errors in the
browser. Technically, this code works just fine. Let’s run ESLint on
this file and see what feedback we get based on our customized rules:
The command eslint . will lint our entire directory. To do this, we’ll
most likely require that ESLint ignore some JavaScript files. The
.eslintignore file is where we can add files or directories for ESLint to
ignore:
dist/assets/
sample.js
This .eslintignore file tells ESLint to ignore our new sample.js file as
well as anything in the dist/assets folder. If we don’t ignore the assets
folder, ESLint will analyze the client bundle.js file, and it will probably
find a lot to complain about in that file.
{
"scripts": {
"lint": "eslint ."
}
}
Now ESLint can be run any time we want with npm run lint, and it
will analyze all of the files in our project except the ones we’ve
ignored.
ESLint Plug-Ins
There are a multitude of plug-ins that can be added to your ESLint
configuration to help you as you’re writing code. For a React project,
you’ll definitely want to install eslint-plugin-react-hooks, a plug-
in to enforce the rules of React Hooks. This package was released by
the React team to help fix bugs related to Hooks usage.
# OR
{
"plugins": [
// ...
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
This plug-in will check to ensure that functions that start with the word
“use” (assumed to be a hook) are following the rules of Hooks.
Once this has been added, we’ll write some sample code to test the
plug-in. Adjust the code in sample.js. Even though this code won’t run,
we’re testing to see if the plug-in is working appropriately:
function gnar() {
const [nickname, setNickname] = useState(
"dude"
);
return <h1>gnarly</h1>;
}
Several errors will pop up from this code, but most importantly, there’s
the error that lets us know we’re trying to call useState in a function
that isn’t a component or a hook:
These shoutouts will help us along the way as we learn the ins and outs
of working with Hooks.
This plug-in will analyze your code and ensure that it’s not breaking
any accessibility rules. Accessibility should be an area of focus for all
of us, and working with this plug-in will promote good practices when
writing accessible React applications.
// or
{
"extends": [
// ...
"plugin:jsx-a11y/recommended"
],
"plugins": [
// ...
"jsx-a11y"
]
}
Now let’s test it. We’ll adjust our sample.js file to include an image tag
that has no alt property. In order for an image to pass a lint check, it
must have an alt prop or an empty string if the image doesn’t affect the
user’s understanding of the content:
function Image() {
return <img src="/img.png" />;
}
If we run lint again with npm run lint, we’ll see that there’s a new
error that’s called by the jsx/a11y plug-in:
5:10 error img elements must have an alt prop, either with meaningful
text,
or an empty string for decorative images
There are many other ESLint plug-ins you can use to statically analyze
your code, and you could spend weeks tuning your ESLint config to
perfection. If you’re looking to take yours to the next level, there are
many useful resources in the Awesome ESLint repository.
Prettier
Prettier is an opinionated code formatter you can use on a range of
projects. The effect Prettier has had on the day-to-day work of web
developers since its release has been pretty incredible. Based on
historical records, arguing over syntax filled 87% of an average
JavaScript developer’s day, but now Prettier handles code formatting
and defining the rules around what code syntax should be used per
project. The time savings are significant. Also, if you’ve ever
unleashed Prettier on a Markdown table, the quick, crisp formatting
that occurs is a pretty incredible sight to behold.
To make Prettier work with ESLint, we’ll tinker with the configuration
of our project a bit more. You can install Prettier globally to get
started:
{
"semi": true,
"trailingComma": none,
"singleQuote": false,
"printWidth": 80
}
These are our preferred defaults, but of course, choose what makes
most sense to you. For more Prettier formatting options, check out
Prettier’s documentation.
Let’s replace what currently lives in our sample.js file with some code
to format:
console.log("Prettier Test")
Now let’s try running the Prettier CLI from the Terminal or Command
Prompt:
Prettier runs the test and shows us the following message: Code style
issues found in the above file(s). Forgot to run
Prettier? To run it from the CLI, we can pass the write flag:
First, we’ll integrate ESLint and Prettier by installing a config tool and
a plug-in:
{
"extends": [
// ...
"plugin:prettier/recommended"
],
"plugins": [
//,
"prettier"],
"rules": {
// ...
"prettier/prettier": "error"
}
}
Make sure to break some formatting rules in your code to ensure that
Prettier is working. For example, in sample.js:
console.log("Prettier Test");
Running the lint command npm run lint will yield the following
output:
All of the errors were found. Now you can run the Prettier write
command and sweep the formatting for one file:
Prettier in VSCode
If you’re using VSCode, it’s highly recommended that you set up
Prettier in your editor. Configuration is fairly quick and will save you a
lot of time as you’re writing code.
You’ll first want to install the VSCode extension for Prettier. Just
follow this link and click Install. Once installed, you can run Prettier
with Control + Command + P on a Mac or Ctrl + Shift + P on a PC to
manually format a file or highlighted bit of code. For even better
results, you can format your code on Save. This involves adding some
settings to VSCode.
To access these settings, select the Code menu, then Preferences, then
Settings. (Or Command + comma on a Mac or Ctrl + comma on a PC,
if you’re in a hurry.) Then you can click on the small paper icon in the
upper right-hand corner to open the VSCode settings as JSON. You’ll
want to add a few helpful keys here:
{
"editor.formatOnSave": true
}
Now when you save any file, Prettier will format it based on the
.prettierrc defaults! Pretty killer. You can also search Settings for
Prettier options to set up defaults in your editor if you want to enforce
formatting, even if your project doesn’t contain a .prettierrc config file.
If you’re using a different editor, Prettier likely supports that, too. For
instructions specific to other code editors, check out the Editor
Integration section of the docs.
PropTypes
In the first edition of this book, PropTypes were part of the core React
library and were the recommended way to add typechecking to your
application. Today, due to the emergence of other solutions like Flow
and TypeScript, the functionality has been moved to its own library to
make React’s bundle size smaller. Still, PropTypes are a widely used
solution.
We’ll test this by creating a minimal App component that renders the
name of a library:
ReactDOM.render(
<App name="React" />,
document.getElementById("root")
);
App.propTypes = {
name: PropTypes.string
};
The App component has one property name and should always be a
string. If an incorrect type value is passed as the name, an error will be
thrown. For example, if we used a boolean:
ReactDOM.render(
<App name="React" />,
document.getElementById("root")
);
Warning: Failed prop type: Invalid prop name of type boolean supplied to
App,
expected string. in App
App.propTypes = {
name: PropTypes.string,
using: PropTypes.bool
};
ReactDOM.render(
<App name="React" using={true} />,
document.getElementById("root")
);
The longer list of type checks includes:
PropTypes.array
PropTypes.object
PropTypes.bool
PropTypes.func
PropTypes.number
PropTypes.string
PropTypes.symbol
Additionally, if you want to ensure that a value was provided, you can
chain .isRequired onto the end of any of these options. For example,
if a string must be supplied, you’d use:
App.propTypes = {
name: PropTypes.string.isRequired
};
ReactDOM.render(
<App />,
document.getElementById("root")
);
Then, if you fail to provide a value for this field, the following warning
will appear in the console:
index.js:1 Warning: Failed prop type: The prop name is marked as required
in App,
but its value is undefined.
There also may be situations where you don’t care what the value is, as
long as a value is provided. In that case, you can use any. For example:
App.propTypes = {
name: PropTypes.any.isRequired
};
In addition to the basic typechecks, there are a few other utilities that
are useful for many real-world situations. Consider a component where
there are two status options: Open and Closed:
ReactDOM.render(
<App status="Open" />,
document.getElementById("root")
);
App.propTypes = {
status: PropTypes.string.isRequired
};
That works well, but if other string values besides Open and Closed are
passed in, the property will be validated. The type of check we actually
want to enforce is an enum check. An enumeration type is a restricted
list of options for a particular field or property. We’ll adjust the
propTypes object like so:
App.propTypes = {
status: PropTypes.oneOf(["Open", "Closed"])
};
Now if anything other than the values from the array that’s passed to
PropTypes.oneOf is supplied, a warning will appear.
For all the options you can configure for PropTypes in your React app,
check out the documentation.
Flow
Flow is a typechecking library that’s used and maintained by Facebook
Open Source. It’s a tool that checks for errors via static type
annotations. In other words, if you create a variable that’s a particular
type, Flow will check to be sure that that value used is the correct type.
Then we’ll add Flow to the project. Create React App doesn’t assume
you want to use Flow, so it doesn’t ship with the library, but it’s
smooth to incorporate:
Once installed, we’ll add an npm script to run Flow when we type npm
run flow. In package.json, just add this to the scripts key:
{
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"flow": "flow"
}
}
Now running the flow command will run typechecking on our files.
Before we can use it, though, we need to create a .flowconfig file. To
do so, we run:
[ignore]
[include]
[libs]
[lints]
[options]
[strict]
In most cases, you’ll leave this blank to use Flow’s defaults. If you
want to configure Flow beyond the basics, you can explore more
options in the documentation.
One of the coolest features of Flow is that you can adopt Flow
incrementally. It can feel overwhelming to have to add typechecking to
an entire project. With Flow, this isn’t a requirement. All you need to
do is add the line //@flow to the top of any files you want to
typecheck, then Flow will automatically only check those files.
Another option is to add the VSCode extension for Flow to help with
code completion and parameter hints. If you have Prettier or a linting
tool set up, this will help your editor handle the unexpected syntax of
Flow. You can find that in the marketplace.
Let’s open the index.js file and, for the sake of simplicity, keep
everything in the same file. Make sure to add //@flow to the top of the
file:
//@flow
function App(props) {
return (
<div>
<h1>{props.item}</h1>
</div>
);
}
ReactDOM.render(
<App item="jacket" />,
document.getElementById("root")
);
type Props = {
item: string
};
Then run Flow npm run flow. In certain versions of Flow, you may
see this warning:
Cannot call ReactDOM.render with root bound to container because null [1]
is
incompatible with Element [2]
In either case, you’ll clear the error and see that your code is free of
errors!
No errors!
We could trust this fully, but trying to break it feels like a good idea.
Let’s pass a different property type to the app:
Cannot create App element because number [1] is incompatible with string
[2]
in property item.
Let’s switch it back and add another property for a number. We’ll also
adjust the component and property definitions:
type Props = {
item: string,
cost: number
};
ReactDOM.render(
<App item="jacket" cost={249} />,
root
);
Cannot create App element because property cost is missing in props [1]
but
exists in Props [2].
type Props = {
item: string,
cost?: number
};
That’s the tip of the iceberg with all of the different features that Flow
has to offer. To learn more and to stay on top of the changes in the
library, head over to the documentation site.
TypeScript
TypeScript is another popular tool for typechecking in React
applications. It’s an open source superset of JavaScript, which means
that it adds additional features to the language. Created at Microsoft,
TypeScript is designed to be used for large apps to help developers find
bugs and iterate more quickly on projects.
We’ll start by generating yet another Create React App, this time with
some different flags:
Now let’s tour the features of our scaffolded project. Notice in the src
directory that the file extensions are .ts or .tsx now. We’ll also find a
.tsconfig.json file, which contains all of our TypeScript settings. More
on that in a bit.
Also, if you take a look at the package.json file, there are new
dependencies listed and installed related to TypeScript, like the library
itself and type definitions for Jest, React, ReactDOM, and more. Any
dependency that starts with @types/ describes the type definitions for
a library. That means that the functions and methods in the library are
typed so that we don’t have to describe all of the library’s types.
NOTE
If your project doesn’t include the TypeScript features, you might be using an old
version of Create React App. To get rid of this, you can run npm uninstall -g
create-react-app.
Let’s try dropping our component from the Flow lesson into our
project. Just add the following to the index.ts file:
function App(props) {
return (
<div>
<h1>{props.item}</h1>
</div>
);
}
ReactDOM.render(
<App item="jacket" />,
document.getElementById("root")
);
If we run the project with npm start, we should see our first
TypeScript error. This is to be expected at this point:
This means we need to add type rules for this App component. We’ll
start by defining types just as we did earlier for the Flow component.
The item is a string, so we’ll add that to the AppProps type:
type AppProps = {
item: string;
};
ReactDOM.render(
<App item="jacket" />,
document.getElementById("root")
);
Now the component will render with no TypeScript issues. It’s also
possible to destructure props if we’d like to:
ReactDOM.render(
<App item={1} />,
document.getElementById("root")
);
The error also tells us the exact line where there’s a problem. This is
extremely useful as we’re debugging.
type AppProps = {
item: string;
};
For more on TypeScript, check out the official docs and the amazing
React+TypeScript Cheatsheets on GitHub.
Test-Driven Development
Test-driven development, or TDD, is a practice—not a technology. It
does not mean that you simply have tests for your application. Rather,
it’s the practice of letting the tests drive the development process. In
order to practice TDD, you should follow these steps:
Write the minimal amount of code required to make the tests pass
(green)
Focus specifically on making each test pass; do not add any
functionality beyond the scope of the test.
Refactor both the code and the tests (gold)
Once the tests pass, it’s time to take a closer look at your code and
your tests. Try to express your code as simply and as beautifully as
possible.2
For the remainder of this chapter, we’ll be writing tests for code that
already exists. Technically, we’re not practicing TDD. However, in the
next section, we’ll pretend that our code does not already exist so we
can get a feel for the TDD workflow.
Incorporating Jest
Before we can get started writing tests, we’ll need to select a testing
framework. You can write tests for React with any JavaScript testing
framework, but the official React docs recommend testing with Jest, a
JavaScript test runner that lets you access the DOM via JSDOM.
Accessing the DOM is important because you want to be able to check
what is rendered with React to ensure your application is working
correctly.
Now we can start thinking about testing with a small example. We’ll
create two new files in the src folder: functions.js and functions.test.js.
Remember, Jest is already configured and installed in Create React
App, so all you need to do is start writing tests. In functions.test.js,
we’ll stub the tests. In other words, we’ll write what we think the
function should do.
functions.test.js
The next thing we’ll do is stub the function that will multiply numbers
by two. This function will be referred to as our system under test
(SUT). In functions.js, create the function:
We’ll export it so that we can use the SUT in the test. In the test file,
we want to import the function, and we’ll use expect to write an
assertion. In the assertion, we’ll say that if we pass 4 to the timesTwo
function, we expect that it should return 8:
Jest “matchers” are returned by the expect function and used to verify
results. To test the function, we’ll use the .toBe matcher. This verifies
that the resulting object matches the argument sent to .toBe.
Let’s run the tests and watch them fail using npm test or npm run
test. Jest will provide specific details on each failure, including a
stack trace:
FAIL src/functions.test.js
✕ Multiplies by two (5ms)
● Multiplies by two
Expected: 8
Received: undefined
2 |
3 | test("Multiplies by two", () => {
> 4 | expect(timesTwo(4)).toBe(8);
| ^
5 | });
6 |
at Object.<anonymous> (src/functions.test.js:4:23)
Taking the time to write the tests and run them to watch them fail
shows us that our tests are working as intended. This failure feedback
represents our to-do list. It’s our job to write the minimal code required
to make our tests pass.
The .toBe matcher has helped us test for equality with a single value.
If we want to test an object or array, we could use .toEqual. Let’s go
through another cycle with our tests. In the test file, we’ll test for
equality of an array of objects.
We have a list of menu items from the Guy Fieri restaurant in Las
Vegas. It’s important that we build an object of their ordered items so
the customer can get what they want and know what they’re supposed
to pay. We’ll stub the test first:
Now we’ll use the order function in the test file. We’ll also assume that
we have a starter list of data for an order that we need to transform:
const menuItems = [
{
id: "1",
name: "Tatted Up Turkey Burger",
price: 19.5
},
{
id: "2",
name: "Lobster Lollipops",
price: 16.5
},
{
id: "3",
name: "Motley Que Pulled Pork Sandwich",
price: 21.5
},
{
id: "4",
name: "Trash Can Nachos",
price: 19.5
}
];
Remember that we’ll use toEqual because we’re checking the value of
an object instead of an array. What do we want the result to equal?
Well, we want to create an object that looks like this:
const result = {
orderItems: menuItems,
total: 77
};
And when we check out the terminal, we’ll find that are tests are now
passing! Now this might feel like a trivial example, but if you were
fetching data, it’s likely that you’d test for shape matches of arrays and
objects.
When you wrap tests in the describe statement, the test runner creates
a block of tests, which makes the testing output in the terminal look
more organized and easier to read:
Math functions
✓ Multiplies by two
✓ Adds two numbers
✓ Subtracts two numbers (1ms)
This process represents a typical TDD cycle. We wrote the tests first,
then wrote code to make the tests pass. Once the tests pass, we can take
a closer look at the code to see if there’s anything that’s worth
refactoring for clarity or performance. This approach is very effective
when working with JavaScript (or really any other language).
We’re not running our tests in a browser; we’re running them in the
terminal with Node.js. Node.js does not have the DOM API that comes
standard with each browser. Jest incorporates an npm package called
jsdom that’s used to simulate a browser environment in Node.js, which
is essential for testing React components.
For each component test, it’s likely that we’ll need to render our React
component tree to a DOM element. To demonstrate this workflow,
let’s revisit our Star component in Star.js:
ReactDOM.render(
<Star />,
document.getElementById("root")
);
Now let’s write our test. We already wrote the code for the star, so we
won’t be partaking in TDD here. If you had to incorporate tests into
your existing apps, this is how you’d do it. In a new file called
Star.test.js, start by importing React, ReactDOM, and the Star:
We’ll also want to write the tests. Remember, the first argument we
supply to test is the name of the test. Then we’re going to perform
some setup by creating a div that we can render the star to with
ReactDOM.render. Once the element is created, we can write the
assertion:
expect(
div.querySelector("notrealthing")
).toBeTruthy();
When you generated your React project, you may have noticed that a
few packages from @testing-library were installed in addition to
the basics like React and ReactDOM. React Testing Library is a project
that was started by Kent C. Dodds as a way to enforce good testing
practices and to expand the testing utilities that were part of the React
ecosystem. Testing Library is an umbrella over many testing packages
for libraries like Vue, Svelte, Reason, Angular, and more—it’s not just
for React.
One potential reason you might choose React Testing Library is to get
better error messages when a test fails. The current error we see when
we test the assertion:
expect(
div.querySelector("notrealthing")
).toBeTruthy();
is:
expect(received).toBeTruthy()
Received: null
Now when we run the tests, we see an error telling us exactly what’s
what:
expect(element).toHaveAttribute("id", "hotdog")
// element.getAttribute("id") === "hotdog"
expect(div.querySelector("svg")).toHaveAttribute(
"id",
"star"
);
Using more than one of the custom matchers just means that you need
to import, extend, and use:
import {
toHaveAttribute,
toHaveClass
} from "@testing-library/jest-dom";
expect(you).toHaveClass("evenALittle");
import "@testing-library/jest-dom/extend-expect";
So if you’re using Create React App, you don’t even have to include
the import in your test files.
Queries
Queries are another feature of the React Testing Library that allow you
to match based on certain criteria. In order to demonstrate using a
query, let’s adjust the Star component to include a title. This will
allow us to write a common style of test—one that matches based on
text:
Let’s pause to think about what we’re trying to test. We want the
component to render, and now we want to test to see if the h1 contains
the correct text. A function that’s part of React Testing Library,
render, will help us do just that. render will replace our need to use
ReactDOM.render(), so the test will look a bit different. Start by
importing render from React Testing Library:
Testing Events
Another important part of writing tests is testing events that are part of
components. Let’s use and test the Checkbox component we created in
Chapter 7:
return (
<>
<label>
{checked ? "checked" : "not checked"}
<input
type="checkbox"
value={checked}
onChange={setChecked}
/>
</label>
</>
);
}
The first thing we need to do is select the element that we want to fire
the event on. In other words, which element do we want to click on
with the automated test? We’ll use one of Testing Library’s queries to
find the element we’re looking for. Since the input has a label, we can
use getByLabelText():
When the component first renders, its label text reads not checked, so
we can search via a regular expression to find a match with the string:
Now we have our checkbox selected. All we need to do now is fire the
event (click the checkbox) and write an assertion to make sure that the
checked property is set to true when the checkbox is clicked:
You also could add the reverse toggle to this checkbox test by firing
the event again and checking that the property is set to false on
toggle. We changed the name of the test to be more accurate:
<input
type="checkbox"
value={checked}
onChange={setChecked}
data-testid="checkbox" // Add the data-testid= attribute
/>
This will do the same thing but is particularly useful when reaching out
to DOM elements that are otherwise difficult to access.
Jest ships with Istanbul, a JavaScript tool used to review your tests and
generate a report that describes how many statements, branches,
functions, and lines have been covered.
To run Jest with code coverage, simply add the coverage flag when
you run the jest command:
This report tells you how much of your code in each file has been
executed during the testing process and reports on all files that have
been imported into tests.
Jest also generates a report that you can run in your browser, which
provides more details about what code has been covered by tests. After
running Jest with coverage reporting, you’ll notice that a coverage
folder has been added to the root. In a web browser, open this file:
/coverage/lcov-report/index.html. It will show you your code coverage
in an interactive report.
This report tells you how much of the code has been covered, as well
as the individual coverage based on each subfolder. You can drill down
into a subfolder to see how well the individual files within have been
covered. If you select the components/ui folder, you’ll see how well
your user interface components are covered by testing.
You can see which lines have been covered in an individual file by
clicking on the filename.
Code coverage is a great tool to measure the reach of your tests. It’s
one benchmark to help you understand when you’ve written enough
unit tests for your code. It’s not typical to have 100% code coverage in
every project. Shooting for anything above 85% is a good target.3
Testing can often feel like an extra step, but the tooling around React
testing has never been better. Even if you don’t test all of your code,
starting to think about how to incorporate testing practices can help you
save time and money when building production-ready applications.
For a brief introduction to unit testing, see Martin Fowler’s article, “Unit Testing”.
1
For more on this development pattern, see Jeff McWherter’s and James Bender’s “Red,
2 Green, Refactor”.
When the web started, most websites consisted of a series of pages that
users could navigate through by requesting and opening separate files.
The location of the current file or resource was listed in the browser’s
location bar. The browser’s forward and back buttons would work as
expected. Bookmarking content deep within a website would allow
users to save a reference to a specific file that could be reloaded at the
user’s request. On a page-based, or server-rendered, website, the
browser’s navigation and history features simply work as expected.
3
companies including Uber, Zendesk, PayPal, and Vimeo.3
In this chapter, we’ll introduce React Router and leverage its features
to handle routing on the client.
The sitemap for this website consists of a home page, a page for each
section, and an error page to handle 404 Not Found errors (see
Figure 11-2).
The router will allow us to set up routes for each section of the website.
Each route is an endpoint that can be entered into the browser’s
location bar. When a route is requested, we can render the appropriate
content.
To start, let’s install React Router and React Router DOM. React
Router DOM is used for regular React applications that use the DOM.
If you’re writing an app for React Native, you’ll use react-router-
native. We’re going to install these packages at their experimental
versions because React Router 6 is not officially out at the time of this
printing. Once released, you can use the packages without that
designation.
With these pages stubbed out, we need to adjust the index.js file.
Instead of rendering the App component, we’ll render the Router
component. The Router component passes information about the
current location to any children that are nested inside of it. The Router
component should be used once and placed near the root of our
component tree:
render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={<About />}
/>
<Route
path="/events"
element={<Events />}
/>
<Route
path="/products"
element={<Products />}
/>
<Route
path="/contact"
element={<Contact />}
/>
</Routes>
</div>
);
}
These routes tell the Router which component to render when the
window’s location changes. Each Route component has path and
element properties. When the browser’s location matches the path,
the element will be displayed. When the location is /, the router will
render the Home component. When the location is /products, the
router will render the Products component.
At this point, we can run the app and physically type the routes into the
browser’s location bar to watch the content change. For example, type
http://localhost:3000/about into the location bar and watch the About
component render.
It’s probably not realistic to expect our users to navigate the website by
typing routes into the location bar. The react-router-dom provides a
Link component that we can use to create browser links.
Let’s modify the home page to contain a navigation menu with a link
for each route:
Now users can access every internal page from the home page by
clicking on a link. The browser’s back button will take them back to
the home page.
Router Properties
The React Router passes properties to the components it renders. For
instance, we can obtain the current location via a property. Let’s use
the current location to help us create a 404 Not Found component.
First, we’ll create the component:
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/about"
element={<About />}
/>
<Route
path="/events"
element={<Events />}
/>
<Route
path="/products"
element={<Products />}
/>
<Route
path="/contact"
element={<Contact />}
/>
<Route path="*" element={<Whoops404 />} />
</Routes>
</div>
);
}
If you log the location, you can explore that object further.
Nesting Routes
Route components are used with content that should be displayed only
when specific URLs are matched. This feature allows us to organize
our web apps into eloquent hierarchies that promote content reuse.
Home Page
About the Company
Company (default)
History
Services
Location
Events
Products
Contact Us
404 Error Page
The new routes that we need to create will reflect this hierarchy:
http://localhost:3000/
http://localhost:3000/about
http://localhost:3000/about
http://localhost:3000/about/history
http://localhost:3000/about/services
http://localhost:3000/about/location
http://localhost:3000/events
http://localhost:3000/products
http://localhost:3000/contact
http://localhost:3000/hot-potato
import {
Home,
About,
Events,
Products,
Contact,
Whoops404,
Services,
History,
Location
} from "./pages";
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="about" element={<About />}>
<Route
path="services"
element={<Services />}
/>
<Route
path="history"
element={<History />}
/>
<Route
path="location"
element={<Location />}
/>
</Route>
<Route
path="events"
element={<Events />}
/>
<Route
path="products"
element={<Products />}
/>
<Route
path="contact"
element={<Contact />}
/>
<Route path="*" element={<Whoops404 />} />
</Routes>
</div>
);
}
Once you’ve wrapped the nested routes with the About Route
component, you can visit these pages. If you open
http://localhost:3000/about/history, you’ll just see the content from the
About page, but the History component doesn’t display. In order to
get that to display, we’ll use another feature of React Router DOM: the
Outlet component. Outlet will let us render these nested components.
We’ll just place it anywhere we want to render child content.
In the About component in pages.js, we’ll add this under the <h1>:
import {
Link,
useLocation,
Outlet
} from "react-router-dom";
export function About() {
return (
<div>
<h1>[About]</h1>
<Outlet />
</div>
);
}
Now this About component will be reused across the entire section and
will display the nested components. The location will tell the app
which subsection to render. For example, when the location is
http://localhost:3000/about/history, the History component will be
rendered inside of the About component.
Using Redirects
Sometimes you want to redirect users from one route to another. For
instance, we can make sure that if users try to access content via
http://localhost:3000/services, they get redirected to the correct route:
http://localhost:3000/about/services.
import {
Routes,
Route,
Redirect
} from "react-router-dom";
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
// Other Routes
<Redirect
from="services"
to="about/services"
/>
</Routes>
</div>
);
}
When routes are changed in a production application, users will still try
to access old content via old routes. This typically happens because of
bookmarks. The Redirect component provides us with a way to load
the appropriate content for users, even if they’re accessing our site via
an old bookmark.
function App() {
let element = useRoutes([
{ path: "/", element: <Home /> },
{
path: "about",
element: <About />,
children: [
{
path: "services",
element: <Services />
},
{ path: "history", element: <History /> },
{
path: "location",
element: <Location />
}
]
},
{ path: "events", element: <Events /> },
{ path: "products", element: <Products /> },
{ path: "contact", element: <Contact /> },
{ path: "*", element: <Whoops404 /> },
{
path: "services",
redirectTo: "about/services"
}
]);
return element;
}
The official docs call the config element, but you can choose to call it
whatever you like. It’s also totally optional to use this syntax. Route is
a wrapper around useRoutes, so you’re actually using this either way.
Choose whichever syntax and style works best for you!
Routing Parameters
Another useful feature of the React Router is the ability to set up
routing parameters. Routing parameters are variables that obtain their
values from the URL. They’re extremely useful in data-driven web
applications for filtering content or managing display preferences.
Let’s revisit the color organizer and improve it by adding the ability to
select and display one color at a time using React Router. When a user
selects a color by clicking on it, the app should render that color and
display its title and hex value.
Using the router, we can obtain the color ID via the URL. For example,
this is the URL we’ll use to display the color “lawn” because the ID for
lawn is being passed within the URL:
http://localhost:3000/58d9caee-6ea6-4d7b-9984-65b145031979
To start, let’s set up the router in the index.js file. We’ll import the
Router and wrap the App component:
render(
<Router>
<App />
</Router>,
document.getElementById("root")
);
Wrapping the App passes all of the router’s properties to the component
and any other components nested inside of it. From there, we can set up
the route configuration. We’ll use the Routes and Route components
instead of useRoutes, but remember that this is always an option if
you prefer that syntax. Start by importing Routes and Route:
import { Routes, Route } from "react-router-dom";
Then add to the App. This application will have two routes: the
ColorList and the ColorDetails. We haven’t built ColorDetails
yet, but let’s import it:
Now, you should see the ColorDetails page appear. Now we know
that the router and our routes are working, but we want this to be more
dynamic. On the ColorDetails page, we want to display the correct
color based on the id that’s found in the URL. To do that, we’ll use the
useParams hook:
If we log params, we’ll see that this is an object that contains any
parameters that are available on the router. We’ll destructure this object
to grab the id, then we can use that id to find the correct color in the
colors array. Let’s use our useColors hook to make this happen:
return (
<div>
<h1>Details</h1>
</div>
);
}
return (
<div>
<div
style={{
backgroundColor: foundColor.color,
height: 100,
width: 100
}}
></div>
<h1>{foundColor.title}</h1>
<h1>{foundColor.color}</h1>
</div>
);
}
Then we’ll call useNavigate, which will return a function we can use
to navigate to another page:
return (
<section
className="color"
onClick={() => navigate(`/${id}`)}
>
// Color component
</section>
);
Routing parameters are an ideal tool to obtain data that affects the
presentation of your user interface. However, they should only be used
when you want users to capture these details in a URL. For example, in
the case of the color organizer, users can send other users links to
specific colors or all the colors sorted by a specific field. Users can also
bookmark those links to return specific data.
In this chapter, we reviewed the basic usage of the React Router. In the
next chapter, we’ll learn how to use routing on the server.
So far, we’ve built small applications with React that run entirely in the
browser. They’ve collected data in the browser and saved the data
using browser storage. This makes sense because React is a view layer;
it’s intended to render UI. However, most applications require at least
the existence of some sort of a backend, and we will need to
understand how to structure applications with a server in mind.
Node.js will allow us to reuse the same code we’ve written in the
browser in other applications such as servers, CLIs, and even native
applications. Let’s take a look at some universal JavaScript:
fetch("https://api.github.com/users/moonhighway")
.then(res => res.json())
.then(console.log);
Here, we’re making a fetch request to the GitHub API, converting the
response to JSON, then calling a function on the JSON results to parse
it.
However, if we try to run the exact same code with Node.js, we get an
error:
fetch("https://api.github.com/users/moonhighway")
^
This error occurs because Node.js does not have a built-in fetch
function like the browser does. With Node.js, we can use isomorphic-
fetch from npm, or use the built-in https module. Since we’ve
already used the fetch syntax, let’s incorporate isomorphic-fetch:
fetch("https://api.github.com/users/moonhighway")
.then(res => res.json())
.then(userDetails);
Loading data from an API with Node.js requires the use of core
modules. It requires different code. In these samples, the userDetails
function is universal, so the same function works in both environments.
function Star({
selected = false,
onClick = f => f
}) {
return (
<div
className={
selected ? "star selected" : "star"
}
onClick={onClick}
></div>
);
}
function Star({
selected = false,
onClick = f => f
}) {
return React.createElement("div", {
className: selected
? "star selected"
: "star",
onClick: onClick
});
}
The app we’ll server render is our Recipes app that we built in
Chapter 5. You can run Create React App and place this code over the
contents of the index.js file:
const data = [
{
name: "Baked Salmon",
ingredients: [
{
name: "Salmon",
amount: 1,
measurement: "lb"
},
{
name: "Pine Nuts",
amount: 1,
measurement: "cup"
},
{
name: "Butter Lettuce",
amount: 2,
measurement: "cups"
},
{
name: "Yellow Squash",
amount: 1,
measurement: "med"
},
{
name: "Olive Oil",
amount: 0.5,
measurement: "cup"
},
{
name: "Garlic",
amount: 3,
measurement: "cloves"
}
],
steps: [
"Preheat the oven to 350 degrees.",
"Spread the olive oil around a glass baking dish.",
"Add the yellow squash and place in the oven for 30 mins.",
"Add the salmon, garlic, and pine nuts to the dish.",
"Bake for 15 minutes.",
"Remove from oven. Add the lettuce and serve."
]
},
{
name: "Fish Tacos",
ingredients: [
{
name: "Whitefish",
amount: 1,
measurement: "l lb"
},
{
name: "Cheese",
amount: 1,
measurement: "cup"
},
{
name: "Iceberg Lettuce",
amount: 2,
measurement: "cups"
},
{
name: "Tomatoes",
amount: 2,
measurement: "large"
},
{
name: "Tortillas",
amount: 3,
measurement: "med"
}
],
steps: [
"Cook the fish on the grill until hot.",
"Place the fish on the 3 tortillas.",
"Top them with lettuce, tomatoes, and cheese."
]
}
];
ReactDOM.render(
<Menu
recipes={data}
title="Delicious Recipes"
/>,
document.getElementById("root")
);
Doing this can be time consuming. The user might have to wait to see
anything load for a few seconds depending on their network speed.
Using Create React App with an Express server, we can create a hybrid
experience of client- and server-side rendering.
Next, we need to set up our project’s server, and we’ll use Express, a
lightweight Node server. Install it first:
Then we’ll create a server folder called server and create an index.js
file inside of that. This file will build a server that will serve up the
build folder but also preload some static HTML content:
app.use(express.static("./build"));
This imports and statically serves the build folder. Next, we want to
use renderToString from ReactDOM to render the app as a static
HTML string:
app.listen(PORT, () =>
console.log(
`Server is listening on port ${PORT}`
)
);
We’ll pass the Menu component to this function because that’s what we
want to render statically. We then want to read the static index.html file
from the built client app, inject the app’s content in the div, and send
that as the response to the request:
{
"presets": ["@babel/preset-env", "react-app"]
}
You’ll add react-app because the project uses Create React App, and
it has already been installed.
module.exports = {
entry: "./server/index.js",
target: "node",
externals: [nodeExternals()],
output: {
path: path.resolve("build-server"),
filename: "index.js"
},
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader"
}
]
}
};
Also, you might run into a webpack error due to a version conflict
between the version you’ve installed with Create React App and the
version we just installed. To fix the conflict, just add a .env file to the
root of the project and add:
SKIP_PREFLIGHT_CHECK=true
Finally, we can add a few extra npm scripts to run our dev commands:
{
"scripts": {
//...
"dev:build-server": "NODE_ENV=development webpack --config
webpack.server.js
--mode=development -w",
"dev:start": "nodemon ./server-build/index.js",
"dev": "npm-run-all --parallel build dev:*"
}
}
Now when we run npm run dev, both of the processes will run. You
should be able to see the app running on localhost:4000. When the
app runs, the content will load in sequence, first as prerendered HTML
and then with the JavaScript bundle.
Using a technique like this can mean faster load times and will yield a
boost in perceived performance. With users expecting page-load times
of two seconds or less, any improved performance can mean the
difference between users using your website or bouncing to a
competitor.
mkdir project-next
cd project-next
npm init -y
npm install --save react react-dom next
mkdir pages
Then we’ll create some npm scripts to run common commands more
easily:
{
//...
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
In the pages folder, we’ll create an index.js file. We’ll write our
component, but we won’t worry about importing React or ReactDOM.
Instead, we’ll just write a component:
Once we’ve created this, we can run npm run dev to see the page
running on localhost:3000. It displays the expected component.
You’ll also notice there’s a small lightning bolt icon in the lower
righthand corner of the screen. Hovering over this will display a button
that reads Prerendered Page. When you click on it, it will take you to
documentation about the Static Optimization Indicator. This means that
the page fits the criteria for automatic static optimization, meaning that
it can be prerendered. There are no data requirements that block it. If a
page is automatically statically optimized (a mouthful, but useful!), the
page is faster to load because there’s no server-side effort needed. The
page can be streamed from a CDN, yielding a super-fast user
experience. You don’t have to do anything to pick up on this
performance enhancement.
What if the page does have data requirements? What if the page cannot
be prerendered? To explore this, let’s make our app a bit more robust
and build toward a component that fetches some remote data from an
API. In a new file called Pets.js:
const linkStyle = {
marginRight: 15,
color: "salmon"
};
Next, we’ll incorporate the Header component into a new file called
Layout.js. This will dynamically display the component based on the
correct route:
The Layout component will take in props and display any additional
content in the component underneath the Header. Then in each page,
we can create content blocks that can be passed to the Layout
component when rendered. For example, the index.js file would now
look like this:
We’ll use this to make a fetch call to the Pet Library API. Start by
importing it in the Pages.js file:
When we return the data as the value for pets, we then can map over
the data in the component.
Adjust the component to map over the pets property:
Once we’re satisfied with the state of the application, we can run a
build with npm run build. Next.js is concerned with performance, so
it will give us a full rundown of the number of kilobytes present for
each file. This is a quick spot-check for unusually large files.
Next to each file, we’ll see an icon for whether a site is server-rendered
at runtime (λ), automatically rendered as HTML (○), or automatically
generated as static HTML + JSON (●).
Once you’ve built the app, you can deploy it. Next.js is an open source
product of Zeit, a cloud-hosting provider, so the experience of
deploying with Zeit is the most straightforward. However, you can use
many different hosting providers to deploy your application.
Rehydration
Loading JavaScript views on the client to reuse the server-rendered
HTML’s DOM tree and data.
Prerendering
Running a client-side application at build time and capturing initial
state as static HTML.
Gatsby
Another popular site generator that’s based on React is Gatsby. Gatsby
is taking over the world as a straightforward way to create a content-
driven website. It aims to offer smarter defaults to manage concerns
like performance, accessibility, image handling, and more. And if
you’re reading this book, it’s likely that you might work on a Gatsby
project at some point!
Gatsby is used for a range of projects, but it’s often used to build
content-driven websites. In other words, if you have a blog or static
content, Gatsby is a great choice, particularly now that you know
React. Gatsby can also handle dynamic content like loading data from
APIs, integration with frameworks, and more.
If you have yarn installed globally, the CLI will ask you whether to use
yarn or npm. Either is fine. Then you’ll change directory into the pets
folder:
cd pets
Now you can start the project with gatsby develop. When you visit
localhost:8000, you’ll see your Gatsby starter site running. Now you
can take a tour of the files.
If you open up the project’s src folder, you’ll see three subfolders:
components, images, and pages.
Within the pages folder, you’ll find a 404.js error page, an index.js
page (the page that renders when you visit localhost:8000), and a
page-2.js that renders the content of the second page.
If you visit the components folder, this where the magic of Gatsby is
located. Remember when we built the Header and Layout components
with Next.js? Both of these components are already created as
templates in the components folder.
layout.js
This contains the Layout component. It uses the useStaticQuery
hook to query some data about the site using GraphQL.
seo.js
This component lets us access the page’s metadata for search
engine optimization purposes.
If you add additional pages to the pages folder, this will add additional
pages to your site. Let’s try it and add a page-3.js file to the pages
folder. Then we’ll add the following code to that file to stand up a
quick page:
That’s the tip of the iceberg with what you can do with Gatsby, but
we’ll leave you with some information about some of its additional
features:
Static content
You can build your site as static files, which can be deployed
without a server.
CDN support
It’s possible to cache your site on CDNs all over the world to
improve performance and availability.
All of these features and more are used to ensure a seamless user
experience. Gatsby has made a lot of decisions for you. That could be
good or bad, but these constraints aim to let you focus on your content.
There are a number of avenues you can travel down, but these skills
you’ve picked up in React will serve you well as you set out to build
your own applications. When you’re doing so, we hope that this book
will serve as a reference and a foundation. Although React and its
related libraries will almost certainly go through changes, these are
stable tools that you can feel confident about using right away.
Building apps with React and functional, declarative JavaScript is a lot
of fun, and we can’t wait to see what you’ll build.
Babel
benefits of, Compiling JavaScript
history of, Babel
working with, Babel
colors
adding to state, Adding Colors to State
placing in context, Placing Colors in Context
retrieving with useContext, Retrieving Colors with useContext
CommonJS, CommonJS
component trees
building, Constructing elements with data
definition of term, React State Management
inspecting, React Developer Tools, Recipes as JSX
managing state from one location, State in Component Trees
sending interactions back up, Sending Interactions Back up a
Component Tree-Sending Interactions Back up a Component Tree
sending state down, Sending State Down a Component Tree
components
controlled components, Controlled Components
creating fetch components, Creating a Fetch Component
enhancing with hooks, Enhancing Components with Hooks
improving component performance, Improving Component
Performance
pure components, Improving Component Performance
shouldComponentUpdate method, shouldComponentUpdate and
PureComponent
testing React components, Testing React Components-Using Code
Coverage
composition, Composition-Composition
concurrent mode, Fiber
const keyword, The const Keyword
context
context providers and consumers, React Context
createContext function, Placing Colors in Context
custom hooks with, Custom Hooks with Context
purpose of, React Context
stateful context providers, Stateful Context Providers
useContext hook, Retrieving Colors with useContext
data management
challenges of, Incorporating Data
GraphQL, Introducing GraphQL-Making a GraphQL Request
render props, Render Props
requesting data
async/await and, Requesting Data
authorized requests, Authorized Requests
fetch requests, Requesting Data
handling promise states, Handling Promise States
saving data locally, Saving Data Locally
sending data with requests, Sending Data with a Request
uploading files with fetch, Uploading Files with fetch
virtualized lists
canceling requests, Canceling Requests
creating fetch components, Creating a Fetch Component
creating fetch hooks, Creating a Fetch Hook
handling multiple requests, Handling Multiple Requests
implementing, Virtualized Lists
memoizing values, Memozing Values
parallel requests, Parallel Requests
purpose of, Virtualized Lists
throttling network speed, Throttling the Network Speed
waiting for values, Waiting for Values
waterfall requests, Waterfall Requests
development environment
GitHub file repository, File Repository
Node package manager, npm
Node.js, Installing Node.js
React Developer Tools, React Developer Tools, Page Setup
Yarn, Yarn
functions
arrow functions, Arrow Functions-Arrow functions and scope
creating, Creating Functions
default parameters, Default Parameters
function declarations, Function Declarations
function expressions, Function Expressions
function returns, Function returns
higher-order, Functional Programming with JavaScript, What It
Means to Be Functional, Higher-Order Functions
passing arguments, Passing arguments
pure functions, Pure Functions
reducer functions, Improving Code with useReducer
Gatsby, Gatsby
GitHub file repository, File Repository
GraphQL, Introducing GraphQL-Making a GraphQL Request
I
immutability, Immutability
imperative programming, Imperative Versus Declarative-Imperative
Versus Declarative
import statements, ES6 Modules, 3. Creating the webpack build
isomorphic applications, Isomorphic Versus Universal
JavaScript
asynchronous, Asynchronous JavaScript-Building Promises
classes, Classes-Classes
compiling, Compiling JavaScript
ES6 modules, ES6 Modules-CommonJS
evolution of, JavaScript for React
expressions, React Elements as JSX-JavaScript expressions
functions, Creating Functions-Arrow functions and scope
objects and arrays, Objects and Arrays-Asynchronous JavaScript
variables, Declaring Variables-Template Strings
objects
destructuring, Destructuring Objects
object literal enhancement, Object Literal Enhancement
R
React
development environment
GitHub repository, File Repository
Node package manager, npm
Node.js, Installing Node.js
React Developer Tools, React Developer Tools
Yarn, Yarn
server-side rendering
isomorphic versus universal rendering, Isomorphic Versus
Universal
need for, React and the Server
with Gatsby, Gatsby
with Next.js, Server Rendering with Next.js-Server Rendering
with Next.js
with React, Server Rendering React-Server Rendering React
TypeScript, TypeScript
U
unit testing
benefits of, React Testing, Using Code Coverage
ESLint, ESLint-ESLint Plug-Ins
Jest, Incorporating Jest-Create React App and Testing
Prettier, Prettier
test-driven development, Test-Driven Development
testing React components, Testing React Components-Using Code
Coverage
typechecking
Flow, Flow
PropTypes library, PropTypes
TypeScript, TypeScript
useRef hook
building components, Using Refs
variables
const keyword, The const Keyword
declaring, Declaring Variables
let keyword, The let Keyword
template strings, Template Strings
Alex Banks and Eve Porcello are software engineers, instructors, and
cofounders of Moon Highway, a curriculum development company in
Northern California. They’ve created courses for LinkedIn Learning
and egghead.io, are frequent conference speakers, and teach workshops
to engineers all over the world.
Colophon
The animal on the cover of Learning React is a wild boar and its babies
(Sus scrofa). The wild boar, also known as wild swine or Eurasian wild
pig, is native to Eurasia, North Africa, and the Greater Sunda Islands.
Because of human intervention, they are one of the widest-ranging
mammals in the world.
Wild boars have short thin legs and bulky bodies with short, massive
trunks. Their necks are short and thick, leading to a large head that
accounts for up to a third of the body’s length. Adult sizes and weights
vary depending on environmental factors such as access to food and
water. Despite their size, they can run up to 25 miles per hour and jump
to a height of 55–59 inches. In the winter, their coat consists of coarse
bristles that overlay short brown downy fur. These bristles are longer
along the boar’s back and shortest around the face and limbs.
Wild boars have a highly developed sense of smell; they have been
used for drug detection in Germany. They also have an acute sense of
hearing, which contrasts with their weak eyesight and lack of color
vision. The boars are unable to recognize a human standing 30 feet
away.