React Programming - The Big Nerd Ranch Guide by Loren Klingman
React Programming - The Big Nerd Ranch Guide by Loren Klingman
Table of Contents
Introduction
Learning Web Development
Learning React
Prerequisites
How This Book Is Organized
How to Use This Book
Typographical conventions
Using an eBook
For the More Curious
Challenges
The Necessary Tools
Installing Google Chrome
Installing Visual Studio Code
Crash Course in the Command Line
Finding the current directory
Changing directories
Creating a directory
Listing files
Getting administrator privileges
Quitting a program
Installing Node.js
Resources
Finding Documentation and Help
1. Create React App
Create React App
Running the Development Server
Navigating Your App
Project metadata and dependencies
public directory
src directory
Customizing Your App
Title
Favicon
Adding Elements
Styles
The Chrome Developer Tools
Conclusion
Are You More Curious?
For the More Curious: npx
For the More Curious: %PUBLIC_URL%
Challenges
Bronze Challenge: Subtitle
Silver Challenge: Canarygram
2. Components
App Organization
Header Component
JSX
Post Component
React Developer Tools
Why name components?
Props
Reusing a Component
Object destructuring
Rendering Lists
Keys
Conclusion
For the More Curious: Testing Prop Changes in the
DevTools
Bronze Challenge: Footer
Silver Challenge: Canarygram Footer
3. User Events
Adding SelectedItem
User Events
Passing Parameters
Conclusion
For the More Curious: Alternate file-naming conventions
For the More Curious: Detaching Event Handlers
Bronze Challenge: Hello, Goodbye
Silver Challenge: Canary Alert
4. State
What Is State?
useState
Updating State
Conclusion
For the More Curious: Components – Functions vs Classes
Gold Challenge: Canary Shuffle
5. Linting
Creating a Project
Updating the Title and Favicon
Linting Overview
Linting Rules
Installing and configuring Airbnb’s ESLint
Running ESLint
Auto-fixing errors
Overriding ESLint rules
Installing the ESLint Extension in Visual Studio Code
Conclusion
For the More Curious: Shortcuts to Running ESLint in
Visual Studio Code
For the More Curious: Linting Before Committing
6. Prop Types
Adding Resources
Header Component
Whitespace issues
Adding the component to App.js
Styling the header
Thumbnail Component
Prop Types
Installing the prop-types library
Thumbnail Prop Types
Rendering Items
Home Component
Reusing Prop Types
Prop Mismatch
Conclusion
For the More Curious: Why Pass Items as a Prop?
For the More Curious: Inlining Images
Bronze Challenge: Add a Prop
7. Styles
Style Scoping
Responsive Design
Media Queries
Hover Effect
Animating the hover effect
Conclusion
For the More Curious: Grid Layout
For the More Curious: Styled Components
For the More Curious: Compiled CSS
Silver Challenge: Custom Effect
8. Interacting with a Server
Setting Up the Server
Creating a proxy
Adding an HTTP request library
useEffect
Confirming the Network Request
Promises
How promises work
Conclusion
For the More Curious: Server Endpoints
For the More Curious: How to Set Up a Proxy in Production
For the More Curious: Cross-Origin Resource Sharing
(CORS)
For the More Curious: useEffect vs Lifecycle Methods
Silver Challenge: Promise Practice
9. Router
Creating Routes
Using Link
Creating the Details Page and Route
Nesting a route
Index route
useParams
Navigating Home from the Header
Navigating from Thumbnails to Details
Conclusion
Silver Challenge: More Routes
10. Conditional Rendering
Getting Additional Item Info
Undefined Items
if Statements
Inline Logical Operators
Fragments
More Logical Operators
The && operator
The || operator
The ?? operator
Boolean Type Conversion
Conclusion
For the More Curious: Truthy and Falsy Values
Silver Challenge: Promote Sale Items
11. useReducer
useReducer vs useState
Implementing useReducer
The Reducer Function
Updating the Quantity
Displaying Information in the Header
Adding Items to the Cart
Action Creators
Conclusion
12. Editing the Cart
Creating the Cart Page
Linking to the cart page
Viewing the Cart’s Contents
Displaying the Cart Contents
Displaying the item title and price
Removing Items from the Cart
Passing dispatch as a prop
Setting up the remove button
Emptying the Cart
Displaying the Subtotal
Conclusion
Bronze Challenge: Increasing the Quantity
Silver Challenge: Decreasing the Quantity
13. Forms
Building the Form
onSubmit
Value from Inputs
Controlled Components
onChange
Calculating the Tax
Calculating the Total
Displaying the total conditionally
Formatting Input
Form Validation
Conclusion
Silver Challenge: Coupon Code
Gold Challenge: Maintaining the Cursor Position in the
Phone Number
Gold Challenge: Setting the Quantity
14. Local Storage and useRef
Local Storage
useRef vs useState
Using setTimeout with useRef
Accessing DOM Elements
Accessibility Considerations
Conclusion
Silver Challenge: Returning Focus to the Name Input
15. Submitting Orders
Submitting a Form
async/await
Comparing async/await and then/catch
Preventing Inadvertent Submissions
Conclusion
Bronze Challenge: Reset the Cart State
Silver Challenge: How Many Orders Are in the Queue?
16. Component Composition
Alert
Conditionally Showing an Alert
Component Composition with Props
Reusing Alert
Emptying the Cart
Conclusion
Gold Challenge: Closeable Alert
17. Context
Getting the Logged-In User
Login Button
Avoiding Prop Drilling
Passing currentUser with Context
useMemo
useContext
Login Component
useNavigate
Logout Button
Login Error
Conclusion
For the More Curious: Login Cookie
Silver Challenge: Items Context
18. Fulfilling Orders
Creating the Orders Component
Orders Link
Fetching and Displaying Orders
Fulfilling Orders
Restricting Access with a Custom Hook
Custom hooks
Adding to the dependency array
Cleaning Up useEffect
Getting New Orders
Proxying a websocket
Using the websocket
Closing the websocket
Conclusion
Bronze Challenge: Smarter Redirect
Bronze Challenge: Import a Custom Hook
Silver Challenge: Loader
Silver Challenge: Error Display
19. Introduction to App Performance Optimization
React Profiler
Memo
Context Changes
useCallback
Expensive Calculations
Comparing useCallback, useMemo, and memo
Cleaning Up
Conclusion
Silver Challenge: Memoized UserDetails
20. Testing Overview
Deciding What Tests to Use
Why are you testing?
What do you want to know?
What does the future look like?
Types of Testing
Static Testing
Unit and Integration Testing
Unit testing for functions
Integration testing for components
Testing the full app
Snapshot testing
End-to-End Testing
End-to-end testing with a live back end
End-to-end testing with a mocked back end
Which to use?
Visual or Screenshot Testing
Conclusion
21. Testing with Jest and the React Testing Library
Housekeeping
Unit Testing
Integration Testing
Testing Thumbnail
Testing Home
Mocking the Server
Testing the Logged-In User’s Username
Testing Navigation
Testing the Add to Cart button
Testing checkout
Verifying order submission
Checkout Error
Mocking console.error
Conclusion
For the More Curious: File Structure for Test Files
For the More Curious: React Testing Library Queries
Bronze Challenge: Testing Removing an Item from the Cart
Silver Challenge: Additional cartReducer Tests
Silver Challenge: Testing Phone Number Formatting
Silver Challenge: Testing Tax Details
Gold Challenge: Additional Login Tests
22. End-to-End Testing
Installing and Configuring Cypress
Configuring scripts
Setting up ESLint for Cypress
Importing commands
Setting the base URL
Testing the Login Flow
Testing the Checkout Flow
Interacting with the server
Testing errors in the checkout flow
Testing the Flow to View and Delete Orders
Clearing the orders
Factoring Out Custom Commands
Invisible Elements
Avoiding Flaky Tests
Conclusion
For the More Curious: Cypress Media Files
For the More Curious: Cypress Network Requests
Silver Challenge: Testing Phone Number Formatting
Silver Challenge: Testing Cart Details
Silver Challenge: Testing Removing an Item from the Cart
Gold Challenge: Additional Tests
23. Building Your Application
Manifest
Building the Application
Deploying Your Application
Conclusion
24. Data Loading
Production Build
Analysis
Large Images
Analyzing JavaScript Files
Lazy Loading
Naming the bundles
Other Considerations
Conclusion
Gold Challenge: Lazy Loading
25. Component Speed
Inspecting Performance
Transitions
Storing Rendered Content
Storing a Rendered Component
Conclusion
Gold Challenge: Caching the Filtered Data
26. Afterword
The Final Challenge
Shameless Plugs
Thank You
Index
List of Figures
List of Tables
List of Examples
1.1. Adding a header (App.js)
1.2. Changing the app title (index.xhtml)
1.3. Adding posts (App.js)
1.4. Adding className attributes (App.js)
2.1. Creating a header component (Header.js)
2.2. Replacing JSX header code with a component (App.js)
2.3. Creating a Post for Barry (Post.js)
2.4. Adding a Post (App.js)
2.5. Passing props using attributes (App.js)
2.6. Using prop values in a component (Post.js)
2.7. Reusing the Post component (App.js)
2.8. Adding style for post-name (App.css)
2.9. Adding a new className (Post.js)
2.10. Destructuring props (Post.js)
2.11. Creating an array of otters (App.js)
2.12. Using map to render each post (App.js)
2.13. Adding a key to each post (App.js)
3.1. Creating the SelectedItem component (SelectedItem.js)
3.2. Using a content wrapper (App.js)
3.3. Adding the SelectedItem component (App.js)
3.4. Adding a function to handle click events (Post.js)
3.5. Setting the onClick attribute equal to the new function
(Post.js)
3.6. Passing the otter’s name (Post.js)
3.7. Refactoring to use an arrow function (Post.js)
4.1. Creating the selectedPostName state (App.js)
4.2. Using selectedPostName to show the featured otter (App.js)
4.3. Passing setSelectedPostName as a prop to Post (App.js)
4.4. Using setSelectedPostName in Post (Post.js)
5.1. Updating the application title (index.xhtml)
5.2. Extending Airbnb’s ESLint (package.json)
5.3. Adding a lint script (package.json)
5.4. Fixing the ESLint jsx-one-expression-per-line error
(App.js)
5.5. Overriding JSX rules (package.json)
5.6. Configuring Airbnb’s ESLint (package.json)
5.7. Enabling auto-fix on save (settings.json)
6.1. Creating the Header component (Header.js)
6.2. Adding the header component to App.js (App.js)
6.3. Creating header styles (Header.css)
6.4. Using header styles (Header.js)
6.5. Adding thumbnail styles (Thumbnail.css)
6.6. Creating the Thumbnail component (Thumbnail.js)
6.7. Adding PropTypes to the Thumbnail component
(Thumbnail.js)
6.8. Rendering the item array (App.js)
6.9. Adding home styling (Home.css)
6.10. Creating the Home component (Home.js)
6.11. Adding the Home component (App.js)
6.12. Adding PropTypes to the Home component (Home.js)
6.13. Exporting the item prop type (item.js)
6.14. Replacing the type of items (Home.js)
7.1. Deleting the App.css import (App.js)
7.2. Adding color to the Header component (Header.css)
7.3. Fixing the style rule (Header.css)
7.4. Adding a media query to Home (Home.css)
7.5. Adding a media query to Header (Header.css)
7.6. Adding a transform CSS rule (Thumbnail.css)
7.7. Adding a transition CSS rule (Thumbnail.css)
8.1. Adding a server proxy (package.json)
8.2. Adding useEffect (App.js)
8.3. Replacing the imported items (App.js)
9.1. Adding a BrowserRouter (App.js)
9.2. Adding a home route (App.js)
9.3. Adding a catchall route (App.js)
9.4. Building the NotFound component (NotFound.js)
9.5. Adding the NotFound component to the route (App.js)
9.6. Building the Details component (Details.js)
9.7. Adding a /details route (App.js)
9.8. Adding a nested route (App.js)
9.9. Adding the Outlet component (Details.js)
9.10. Refactoring the DetailItem component (DetailItem.js)
9.11. Adding DetailItem to the route (App.js)
9.12. Adding an index route (App.js)
9.13. Using the useParams hook (DetailItem.js)
9.14. Adding a header link (Header.js)
9.15. Adding the itemId prop to Details (Details.js)
9.16. Adding the itemId prop to Home (Home.js)
9.17. Updating the anchor tag to a Link (Thumbnail.js)
10.1. Adding the items prop (App.js)
10.2. Displaying all item info (DetailItem.js)
10.3. Adding conditional loading (App.js)
10.4. Using a ternary to display the loading message (App.js)
10.5. Using a ternary in DetailItem (DetailItem.js)
10.6. Using the logical AND operator (DetailItem.js)
10.7. Using the logical OR operator (DetailItem.js)
10.8. Using the ?? operator (DetailItem.js)
11.1. Creating the cart reducer variables (cartReducer.js)
11.2. Adding useReducer (App.js)
11.3. Updating cartReducer (cartReducer.js)
11.4. Using action variables (cartReducer.js)
11.5. Passing cart as a prop (App.js)
11.6. Adding the cart icon to the header (Header.js)
11.7. Passing dispatch as a prop (App.js)
11.8. Adding an Add to Cart button (DetailItem.js)
11.9. Adding an action creator (App.js)
11.10. Using the action creator in DetailItem (DetailItem.js)
12.1. Creating the Cart component (Cart.js)
12.2. Adding the /cart route (App.js)
12.3. Updating the Link for the cart icon (Header.js)
12.4. Passing the cart prop (App.js)
12.5. Building the cart contents display (Cart.js)
12.6. Passing items as a prop (App.js)
12.7. Displaying the item title (Cart.js)
12.8. Building the CartRow component (CartRow.js)
12.9. Refactoring the cart table (Cart.js)
12.10. Adding a REMOVE action to the reducer (cartReducer.js)
12.11. Passing dispatch from App to Cart (App.js)
12.12. Passing dispatch from Cart to CartRow (Cart.js)
12.13. Adding a button to remove items (CartRow.js)
12.14. Adding the empty cart message (Cart.js)
12.15. Adding the subtotal (Cart.js)
13.1. Building the checkout form (Cart.js)
13.2. Adding phone and ZIP code inputs (Cart.js)
13.3. Adding a submit button (Cart.js)
13.4. Adding onSubmit (Cart.js)
13.5. Targeting individual inputs (Cart.js)
13.6. Setting name’s value to "M. Mouse" (Cart.js)
13.7. Adding useState (Cart.js)
13.8. Adding state for the phone number and ZIP code (Cart.js)
13.9. Updating onSubmit (Cart.js)
13.10. Computing the tax (Cart.js)
13.11. Computing the total (Cart.js)
13.12. Displaying the total conditionally (Cart.js)
13.13. Formatting the phone number (Cart.js)
13.14. Marking elements as required (Cart.js)
13.15. Disabling the button (Cart.js)
14.1. Setting local storage (App.js)
14.2. Getting local storage (App.js)
14.3. Tracking component renders using state (Cart.js)
14.4. Tracking component renders using a ref (Cart.js)
14.5. Removing experimental code (Cart.js)
14.6. Determining eligible employees (Cart.js)
14.7. Adding setTimeout (Cart.js)
14.8. Using clearTimeout (Cart.js)
14.9. Creating a ZIP code ref (Cart.js)
14.10. Advancing the focus to zipRef’s DOM node (Cart.js)
14.11. Adding accessibility instructions (Cart.js)
15.1. Adding a POST request (Cart.js)
15.2. Adding a success message (Cart.js)
15.3. Adding an error message (Cart.js)
15.4. Adding the isSubmitting state (Cart.js)
16.1. Building the Alert component (Alert.js)
16.2. Adding an alert (Cart.js)
16.3. Adding the visible prop (Alert.js)
16.4. Showing alerts based on state (Cart.js)
16.5. Adding an inline style (Alert.js)
16.6. Using the type prop (Cart.js)
16.7. Adding an error alert (Cart.js)
16.8. Resetting state (Cart.js)
16.9. Adding the EMPTY action to cartReducer (cartReducer.js)
16.10. Dispatching the EMPTY action (Cart.js)
17.1. Getting current user information (App.js)
17.2. Building UserDetails (UserDetails.js)
17.3. Adding UserDetails to Header (Header.js)
17.4. Creating a context (CurrentUserContext.js)
17.5. Adding the context provider (App.js)
17.6. Adding a context display name (CurrentUserContext.js)
17.7. Passing setCurrentUser (App.js)
17.8. Adding useMemo (App.js)
17.9. Subscribing to CurrentUserContext (UserDetails.js)
17.10. Building the Login component (Login.js)
17.11. Adding the login route (App.js)
17.12. Handling the submit event (Login.js)
17.13. Adding useNavigate (Login.js)
17.14. Adding a logout button (UserDetails.js)
17.15. Login error Alert (Login.js)
18.1. Creating the Orders component (Orders.js)
18.2. Adding the orders route (App.js)
18.3. Adding the orders link (UserDetails.js)
18.4. Fetching orders from the server (Orders.js)
18.5. Displaying orders (Orders.js)
18.6. Deleting an order (Orders.js)
18.7. Adding a custom hook (CurrentUserContext.js)
18.8. Importing a custom hook (Orders.js)
18.9. Adding the currentUser dependency (Orders.js)
18.10. Adding a cleanup function (Orders.js)
18.11. Removing the old proxy (package.json)
18.12. Setting up the new proxy (setupProxy.js)
18.13. Adding a websocket (Orders.js)
18.14. Closing the websocket (Orders.js)
19.1. Details memoization (Details.js)
19.2. Using a wrapper component (DetailItem.js)
19.3. useCallback (App.js)
19.4. Adding console.log (Cart.js)
19.5. Adding useMemo (Cart.js)
19.6. Removing console.log (Cart.js)
21.1. Adding a unit test (cartReducer.test.js)
21.2. Testing updates to the item quantity (cartReducer.test.js)
21.3. More cartReducer tests (cartReducer.test.js)
21.4. Adding a Thumbnail test (Thumbnail.test.js)
21.5. Adding data-testid to Thumbnail (Thumbnail.js)
21.6. Adding a Home test (Home.test.js)
21.7. MSW handlers (handlers.js)
21.8. MSW server (server.js)
21.9. Modifying the setup file (setupTests.js)
21.10. Adding a username test to App (App.test.js)
21.11. Testing the user’s starting point (App.test.js)
21.12. Testing navigation to the item details (App.test.js)
21.13. Adding a test ID for the cart quantity (Header.js)
21.14. Adding items to the cart (App.test.js)
21.15. Adding a mock orders request (handlers.js)
21.16. Testing order submission (App.test.js)
21.17. Adding mock order data (data.js)
21.18. Using the data layer in the handlers (handlers.js)
21.19. Resetting the data layer (setupTests.js)
21.20. Checking the number of orders (App.test.js)
21.21. Testing the checkout error (Cart.test.js)
21.22. Mocking the console (Cart.test.js)
22.1. Adding Cypress scripts (package.json)
22.2. Updating the ESLint configuration (package.json)
22.3. Ignoring a lint rule in the Cypress configuration file
(cypress.config.js)
22.4. Adding the Cypress plugin (.eslintrc.js)
22.5. Importing Cypress commands (commands.js)
22.6. Setting the base URL (cypress.config.js)
22.7. Adding a login test (login.cy.js)
22.8. Checkout test (checkout.cy.js)
22.9. Removing the specific number of items (checkout.cy.js)
22.10. Cart error test (checkout.cy.js)
22.11. Testing console.error (checkout.cy.js)
22.12. Adding a view-and-delete orders test (orders.cy.js)
22.13. Clearing all orders (orders.cy.js)
22.14. Adding the login command (commands.js)
22.15. Replacing the login workflow with a command
(login.cy.js)
22.16. Adding the checkout command (commands.js)
22.17. Replacing the checkout workflow with a command
(checkout.cy.js)
22.18. Refactoring the orders test (orders.cy.js)
22.19. Hiding elements with CSS (Cart.css)
22.20. Reverting the CSS change (Cart.css)
23.1. Updating the manifest (manifest.json)
24.1. Using a smaller penguin image file (Header.js)
24.2. Adding lazy loading (App.js)
24.3. Adding chunk names (App.js)
25.1. Saving data in state (Data.js)
25.2. Starting a transition (Data.js)
25.3. Caching table rows (Data.js)
25.4. Caching the Stats component (Stats.js)
25.5. Memoizing setExactAge (Data.js)
React Programming: The Big Nerd
Ranch Guide
by Loren Klingman and Ashley Parker
ISBN-10: 0137901690
ISBN-13: 978-0137901692
The authors and publisher have taken care in writing and printing this book
but make no expressed or implied warranty of any kind and assume no
responsibility for errors or omissions. No liability is assumed for incidental
or consequential damages in connection with or arising out of the use of the
information or programs contained herein.
Abstract
Learn to program React web applications from the pros at Big Nerd Ranch.
Acknowledgements
While our names appear on the cover, many people helped make this book a
reality. We would like to take this chance to thank them.
Eric Wilson, for guiding the process and handling the logistics of
finding the required time and reviewers.
Liz Holaday, our faithful editor, for many rounds of suggestions and
revisions.
Samantha Eng, our copyeditor, who made the text clearer and more
concise.
Yellowstone National Park and the U.S. Fish and Wildlife Service, for
the public domain otter images.
Lastly, thank you to the countless students who have taken Big Nerd
Ranch’s React training. Without your curiosity and your questions, none of
this would matter. This work is a reflection of the insight and inspiration
you have given us over the span of those many weeks. We hope the coffee
made the training a little lighter.
Introduction
Learning Web Development
Front-end development requires a shift in perspective if you have never
built anything for the browser. Here are a few things to keep in mind as you
get started.
Perhaps you have done native development for iOS or Android; written
server-side code in Go, Ruby, or PHP; or built desktop applications for
macOS or Windows. Development of those kinds targets platforms that
might have large reaches but are not universal.
As a front-end developer, you will write code that targets the browser – the
only platform available on nearly every mobile phone, tablet, and personal
computer in the world.
At one end of the spectrum is the look and feel of a web page: rounded
corners, shadows, colors, fonts, whitespace, and so on. At the other end of
the spectrum is the logic that governs the intricate behaviors of that web
page: swapping images in an interactive photo gallery, adding items to a
cart, validating data entered into a form, and so on. You will need to gain
proficiency in several core technologies and understand how they work
together to build great web applications.
No single company controls the standards for web browsers. This means
that front-end developers do not get a yearly SDK release that contains all
the changes they will need to deal with for the next 12 months.
Native platforms are a frozen pond on which you can comfortably skate.
The web is a river; it curves, moves quickly, and is rocky in some places –
and that is part of its appeal. The web is the most rapidly evolving platform
available. Adapting to change is a way of life for a front-end developer.
Learning React
We designed this book to teach you how to write React programs for the
browser. Though we touch on other front-end technologies, such as HTML
and CSS, our focus is React. Because experience is the best teacher, you
will build two React applications as you work through the book.
Prerequisites
This book is not an introduction to programming. It assumes you have
experience with the fundamentals of writing code. It expects you to be
familiar with HTML, CSS, and JavaScript. If you have not worked on front-
end development before, you would benefit from starting with materials to
learn HTML and CSS.
How This Book Is Organized
This book walks you through writing two web applications:
React hooks
ESLint
React Router
Websockets
Cypress
How to Use This Book
This is not a reference book. Its goal is to get you started with React development so you can get the
most out of the reference and recipe books available. It is based on our class at Big Nerd Ranch and,
as such, is meant for you to work through in succession.
That is how students in our classes work through these materials. But they also benefit from having
the right environment, which includes a group of motivated peers and an instructor to answer
questions.
Participate in the forum for this book at forums.bignerdranch.com, where you can discuss the
book and find errata and solutions.
Typographical conventions
To make this book easier to read, certain items appear in certain fonts. Variables, constants, and types
appear in a fixed-width font. Function and method names appear in a bold fixed-width font.
All code listings are in a fixed-width font. Code that you need to type in is always bold. Code that
should be deleted is struck through. For example, in the following change, you are deleting the title
React App and adding the title Ottergram.
<head>
...
<title>React App</title>
<title>Ottergram</title>
</head>
Using an eBook
If you are reading this book on an eReader, we want to point out that reading the code may be tricky
at times. Longer lines of code may wrap to a second line, depending on your selected font size.
The longest lines of code in this book are 86 monospace characters, like this one.
<h1>React Programming: The Big Nerd Ranch Guide, Loren Klingman and Ashley Parker</h1>
You can play with your eReader’s settings to find the best for viewing long code lines.
If you are reading on an iPad with iBooks, we recommend you go to the Settings app, select iBooks,
and set Full Justification OFF and Auto-hyphenation OFF.
When you get to the point where you are actually typing in code, we suggest opening the book on
your PC or Mac in Adobe Digital Editions. (Adobe Digital Editions is a free eReader application you
can download from adobe.com/products/digitaleditions.) Make the application window large enough
so that you can see the code with no wrapping lines. You will also be able to see the figures in full
detail.
For the More Curious
Many chapters in this book end with one or more “For the More Curious”
sections. These sections offer deeper explanations or additional information
about topics presented in the chapter. Though the information in these
sections is not absolutely essential to understanding or completing the
projects this book, we hope you will find it interesting and useful.
Challenges
Challenges are opportunities to review what you have learned and take your
work in the chapter a step further. We recommend that you tackle as many
of them as you can to cement your knowledge and gain a deeper
understanding of the concepts discussed.
Gold challenges are difficult and can take hours to complete. They
require you to understand the concepts from the chapter and then do
some quality thinking and problem solving on your own. Tackling
these challenges will prepare you for the real-world work of React
development.
Before beginning a challenge, make a copy of your project and attack the
challenge in that copy. Many chapters build on previous chapters, and
working on challenges in a copy of the project ensures that you will be able
to progress through the book.
The Create React App tool you will use to create your projects makes a Git
repository of your projects automatically. If you are familiar with Git, you
can use branches to create separate workspaces for challenges. You can also
copy and paste the files in your file explorer to create a copy.
The Necessary Tools
To get started with this book, you will need three basic tools: a browser, a
text editor, and a way to run a JavaScript application.
There are countless tools and resources you can use for React development,
with more being built all the time. For the purposes of this book, we
recommend that you use the same software we use, so you can get the most
benefit from the directions and screenshots.
This chapter walks you through installing and configuring the Google
Chrome browser, the Visual Studio Code text editor, and Node.js. You will
also learn about good documentation options and get a crash course in using
the command line on Mac and Windows. In the next chapter, you will put all
these resources to use as you begin your first project.
You should see a window that looks like Figure 4, “Mac command line”.
To access the command line on Windows, go to the Start menu and search for
“PowerShell.” Find and open the program named Windows PowerShell
(Figure 5, “Finding the PowerShell on Windows”).
From now on, we will refer to “the terminal” or “the command line” to mean
both the Mac Terminal and the Windows PowerShell.
If you are unfamiliar with using the command line, here is a short walk-
through of some common tasks. If you already use the command line, you
can skip ahead to the section called “Installing Node.js” later in this chapter.
You enter all terminal commands by typing at the prompt and pressing the
Return key.
The command line is location based. This means that at any given time, it is
“in” a particular directory within the file structure, and any commands you
enter will apply within that directory. The command-line prompt shows an
abbreviated version of the path of the directory it is in.
To see the whole path, enter the command pwd (which stands for “print
working directory”), as shown in Figure 7, “Showing the current path using
pwd on a Mac” and Figure 8, “Showing the current path using pwd on
Windows”.
To move around the file structure, you use the command cd, or “change
directory,” followed by the path of the directory you want to move into.
cd Documents
cd ..
Now navigate to the directory where you would like to create the projects in
this book. This might be Documents or Desktop or Projects or Sites,
depending on how you like to organize things on your machine. For example,
you might move back into Documents:
cd Documents
Creating a directory
To see this command in action, set up a directory for the projects you will
build as you work through this book. Enter this command:
mkdir react-book
Next, create a new directory to house needed resources, which you will add
later in this chapter. Make it a subdirectory of react-book. You can do this
from your home directory by prefixing the new directory name with the name
of the projects directory and, on a Mac, a slash:
mkdir react-book/resources
mkdir react-book\resources
Remember that you can check your current directory by using the pwd
command. Figure 9, “Changing and checking directories on a Mac” and
Figure 10, “Changing and checking directories on Windows” show examples
of creating directories, moving between them, and checking the current
directory.
Figure 9. Changing and checking directories on a Mac
You might need to see a list of files in your current directory. You can do this
using the ls command. If you want to list the files in another directory, you
can supply a path:
ls
ls react-book
Figure 11, “Using ls to list files in a directory on a Mac” and Figure 12,
“Using ls to list files in a directory on Windows” show this in action:
sudo will prompt you for your password before it runs the command as the
superuser. As you type, the terminal will not echo your keystrokes back, so
type carefully.
Quitting a program
As you proceed through the book, you will run many apps from the command
line. Although some of them will do their job and quit automatically, others
will run until you stop them. To quit a command-line program, press Control-
C (on both Mac and Windows).
Installing Node.js
Node.js lets you use JavaScript programs from the command line. Most
front-end development tools, including React, are written for use with
Node.js.
You can check whether you have Node.js installed on your computer by
entering the following command in your terminal:
node --version
The version of Node.js that this book uses is 18.12.0. Using an older version
of Node.js could result in errors as you work through the book. If you do not
have Node.js installed or if you need to update the version, follow the steps
below.
(If you already have Node.js installed and want to change versions, you must
uninstall your existing version first. A new install will not overwrite an
existing install.)
When you install Node.js, it provides two command-line programs: node and
npm. The node program does the work of running programs written in
JavaScript. The npm command-line tool can perform a variety of tasks, such
as installing third-party code that you can incorporate into your project and
managing your project’s workflow and external dependencies.
The Mozilla Developer Network (MDN) is the best reference for anything
to do with HTML, CSS, and JavaScript. You can access it at
developer.mozilla.org.
The application you will build is Ottergram: a photo-sharing site for sea
otters, some of the most charming marine animals. Visitors to the site will be
able to scroll through the image feed and select a post for more information.
Figure 1.1, “Preview of the completed Ottergram app” shows what the
completed application will look like when a post from the feed is selected:
Create React App uses Babel and webpack behind the scenes to build and run
your app. Though you can configure these tools yourself, that is outside the
scope of this book. When you use Create React App, these tools are
preconfigured and hidden, so you can focus on developing in React.
If you see a prompt asking you to confirm that you want to install create-
react-app, press Y to continue.
Creating a new React application can take a few minutes. When Create React
App has finished, the terminal output will show a list of dependencies that
have been installed and the available commands you can run in your new
application (Figure 1.2, “Creating a new React app”).
cd ottergram
Create React App ships with its own development server to build and serve
the application for you. The development server runs locally with Node.js,
which you installed in The Necessary Tools.
Although you have not added any content to Ottergram, it is useful to start
the development server right away to verify that the initial setup is working
as expected. To start the server, run this command:
npm start
This command will build your app and start a server so you can access it
locally. It will also open a new browser window and navigate to
http://localhost:3000. (If you see a prompt asking you to allow the terminal to
open a browser window, click OK.)
You will typically want to leave the development server running while you
make changes to your application. To stop the server when you are finished
working, press Control-C (Ctrl-C) in the terminal window where the
development server is running.
Navigating Your App
Open Visual Studio Code and take a look at the files Create React App generated.
Open your project by selecting File → Open Folder… in the menu bar (Figure 1.4,
“Opening a folder in Visual Studio Code”).
If you see a prompt asking you to confirm that you trust the authors of the files in
this folder (Figure 1.6, “Prompt to trust the authors”), click Yes, I trust the
authors to give Visual Studio Code access to the ottergram directory.
Figure 1.7. Initial files and folders generated by Create React App
If you do not see a list of files in the left pane, click the explorer icon so you can
inspect the files that Create React App generated for you.
Click the package.json file you see in your top-level ottergram directory.
{
"name": "ottergram",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
{
"name": "ottergram",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "ottergram",
"version": "0.1.0",
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.2.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
},
...
},
"dependencies": {
"": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/[...]",
"integrity": "sha512-[...]",
"requires": {
...
}
},
...
}
}
Note that if you use version control, you should always commit package-
lock.json along with package.json.
In Visual Studio Code’s left sidebar, click the icon next to the node_modules
directory to expose its contents. This directory contains a cache of all installed
dependencies. If you compare it to the specifications in package-lock.json, you
will find that they match exactly.
The node_modules directory is already large and will grow each time you add a
package. Because of this, developers usually choose not to commit it into version
control. You can rebuild it by running the command npm install, which uses
package.json and package-lock.json to install the exact package versions
needed.
Go ahead and close the node_modules directory; you will not be working with its
contents.
public directory
Now, click the icon next to the public directory to expose its contents. public
is the root folder of the React application and contains static files that are served to
the browser (Figure 1.8, “public directory explorer”).
The markup includes the node <div id='root'></div> in the body. React will
look for this HTML node and render all the React code in it.
Also, this file does not contain any script tags, which means it does not load any
external JavaScript files. We mentioned earlier that package.json contains a
dependency called react-scripts. This dependency includes all scripts and
configuration needed to get your React application up and running.
src directory
The src directory contains the example React app and is where you will spend
most of your time in development. Most of the files in this folder are sample files
to get you started. Expose the contents of the directory and take a look (Figure 1.9,
“src directory explorer”).
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Find and open the src/App.js file. It defines a function called App. Take a look at the
function’s return statement:
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
Although this looks a lot like a normal HTML file, it is actually a slightly different
language called JSX. You will learn more about JSX in Chapter 2, Components. For
now, you can think of it as being like HTML.
Begin your work on Ottergram by replacing the placeholder content with your app’s
name. To do this, delete the sample code in the return statement (as well as one of the
import statements) and add a simple header.
function App() {
return (
<div className="App">
<div>
<header className="App-header">
<header>
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<h1>Ottergram</h1>
</header>
...
Save the file with File → Save or Command-S (Ctrl-S). Switch to your browser.
Your app should still be running at http://localhost:3000. If it is not, run the command
npm start from your ottergram directory in the terminal.
Now, instead of the default React logo and text, you should see your header
(Figure 1.10, “Viewing the header in the browser”). Although it is plain, now your
users will know that they have located the otter social media application they were
looking for.
Although the page text says “Ottergram,” the title on the browser tab still says “React
App.” The title displayed on the tab plays a key role in search engine optimization and
getting users to your site. Let’s make sure the title accurately reflects the application.
Go back to public/index.xhtml in Visual Studio Code. Locate the <title> tag in the
<head> section of the HTML. Change the text from React App to Ottergram and save
the file.
<!DOCTYPE html>
<html lang="en">
<head>
...
<title>React App</title>
<title>Ottergram</title>
</head>
...
Now the tab’s title matches your header (Figure 1.11, “Viewing the new title in the
browser”). (If it does not, manually refresh the page in your browser. Sometimes
changes to files in the public directory do not propagate automatically.)
Favicon
Though the Ottergram title is an improvement, the tab still shows the React logo. A
better fit for your app’s branding would be a logo or image of an otter. Like the title, the
favicon – the image shown in the tab – represents your site across the web. It is a visual
identifier that helps users recognize your application.
In your downloaded resources file from The Necessary Tools, the ottergram-
resources directory has favicon images as well as other images you will need for the
app.
In your file explorer, copy the favicon.ico, logo192.png, and logo512.png files from
ottergram-resources and use them to replace the files with the same names in your
project’s public directory.
Return to the browser and reload the page. Yay! Now you have an otter (Figure 1.12,
“New favicon in the browser tab”).
Create React App set up references to two of the favicon files in public/index.xhtml.
Back in Visual Studio Code, open public/index.xhtml to find the links with the
relationships of icon and apple-touch-icon.
...
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
...
Most browsers on web and mobile will use the standard icon link to display the
favicon. Apple’s iOS is unique and uses the apple-touch-icon link to select the
favicon on mobile devices.
There are also references to the favicon files in public/manifest.json, which is used
by some browsers. Open public/manifest.json to take a look:
...
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
...
Adding Elements
You are ready to add some otter posts to the home page so that users can start
building their followings. You will begin by adding the provided images to
your app.
You added the favicon images to the public directory. This allows other files
in the same directory to use the favicon. But files in the public directory are
not part of the webpack build process. So while your otter images are static
files that could live outside the build process, if you put them in the public
directory, you will miss out on some optimization that webpack takes care of
for you.
Instead, you will add the files in src, in a new subdirectory called otters.
In your file explorer, copy the entire otters folder from your downloaded
resources file to src/otters.
There are five otter images. Each post on Ottergram will consist of an image
and the otter’s name, so there will be five posts.
Add the posts to the home page in App.js. Put them in an unordered list, and
make each list item a button, because the posts will eventually be clickable.
import './App.css';
import Barry from './otters/otter1.jpg';
import Robin from './otters/otter2.jpg';
import Maurice from './otters/otter3.jpg';
import Lesley from './otters/otter4.jpg';
import Barbara from './otters/otter5.jpg';
function App() {
return (
<div>
<header>
<h1>Ottergram</h1>
</header>
<ul>
<li>
<button>
<img src={Barry} alt='Barry'/>
<p>Barry</p>
</button>
</li>
<li>
<button>
<img src={Robin} alt='Robin'/>
<p>Robin</p>
</button>
</li>
<li>
<button>
<img src={Maurice} alt='Maurice'/>
<p>Maurice</p>
</button>
</li>
<li>
<button>
<img src={Lesley} alt='Lesley'/>
<p>Lesley</p>
</button>
</li>
<li>
<button>
<img src={Barbara} alt='Barbara'/>
<p>Barbara</p>
</button>
</li>
</ul>
</div>
...
Here again, the function in App.js returns basic HTML tags. But there is a
difference between a simple HTML file and the code above.
You import the image files at the top of your JavaScript file, then you pass
each image file to an <img> tag as a variable. Importing files this way means
they will automatically be bundled into the React build output. React will
take care of getting the file path correct when the app is built, so you do not
have to worry about where the image will live after deployment.
React appends extra characters to the image name to prevent browsers from
caching old images. You can also tell React to use Base64 encoding to embed
the images in the HTML and avoid an extra trip to the server.
One important caveat is that you can only import files this way in JavaScript
files. That is why you did not import the favicon in index.xhtml.
Save your file and return to the browser to see the otters’ posts (Figure 1.13,
“A list of otters”).
Stylesheets are imported just like images and other files. Go back to App.js.
The file already has an import for App.css at the top:
import './App.css';
Open src/App.css. It contains default style rules that Create React App
added. You do not need these rules anymore, since you removed the default
code that they applied to.
For simplicity, instead of editing App.css directly, you will replace it with a
stylesheet provided in your downloaded resources file. In your file explorer,
replace src/App.css with the ottergram-resources/App.css file from your
downloaded resources file.
After replacing App.css, switch back to the browser. Most of the new styles
are not being applied, even if you reload the page. (The background color is
an exception.)
Why not?
body {
background-color: #95c2d7;
font-family: Arial, sans-serif;
}
.header-component h1 {
...
}
.post-list {
...
}
.post-component {
...
}
...
CSS type selectors, such as body, find all matching HTML elements in your
application and apply the specified style rules to those elements. So the
background-color style is being applied to the <body> in your code,
resulting in a lovely ocean-blue background.
On the other hand, CSS class selectors, such as the .header-component and
.post-list selectors in App.css, are not automatically associated with any
elements. You need to add these selectors to the tags in App.js.
In HTML, you associate CSS class selectors with elements by using the
keyword class as an attribute. JSX supports attributes as well – you used
them earlier when you added src attributes to the image tags. But because
class is a reserved word in JavaScript, React uses the attribute className to
apply classes to elements.
Add classNames to the elements in App.js to match the CSS selectors in the
stylesheet:
...
function App() {
return (
<div>
<header>
<header className='header-component'>
<h1>Ottergram</h1>
</header>
<ul>
<ul className='post-list'>
<li>
<li className='post-component'>
...
</li>
<li>
<li className='post-component'>
...
</li>
<li>
<li className='post-component'>
...
</li>
<li>
<li className='post-component'>
...
</li>
<li>
<li className='post-component'>
<button>
...
That is much better – now Ottergram has some nice colors reminiscent of sea
otters’ ocean homes (Figure 1.14, “Ottergram, with style”).
To open the Chrome DevTools, click the triple-dot icon to the right of the
address bar in Chrome. Next, click More Tools → Developer Tools
(Figure 1.15, “Opening the Chrome Developer Tools”).
In Figure 1.16, “Viewing the Chrome DevTools”, you can see the DevTools
next to the web page, showing the Elements tab. The Elements tab displays
all the HTML elements rendered as part of your application. Selecting an
element shows you additional information about the element, including any
applied styles.
Click the Inspect Element button at the left end of the DevTools menu bar
(Figure 1.17, “Inspect Element button”):
Now, move your cursor over the word OTTERGRAM on the web page. As you
hover over the word, the DevTools surround the header with a blue- and
peach-colored rectangle (Figure 1.18, “Inspecting the header”):
In the next chapter, you will build on the work you did here by taking
advantage of a powerful React tool called components.
Are You More Curious?
Many chapters in this book end with one or more “For the More Curious”
sections. These sections offer more details on topics from the chapter. You
do not need to make any changes to your projects based on the code shown
in these sections.
For the More Curious: npx
npx is a tool for executing npm package binaries. It starts with binaries
within the local project and $PATH. If it does not find the package there, it
will download a temporary copy of the latest version and execute the binary
from that download.
Use npx with care. When you use it, you are executing code that has been
downloaded from the internet on your local machine. Take care to vet the
packages that you run – or read the source code yourself.
When you build your app, %PUBLIC_URL% will resolve to the correct absolute
path. It does not matter if your app is located at the URL root level
(localhost:3000/) or not (for example,
localhost:3000/react/ottergram/). This also works for client-side
routing.
Challenges
Challenges are opportunities to review what you have learned and take your
work in the chapter a step further. We recommend that you tackle as many
of them as you can to cement your knowledge and gain a deeper
understanding of the concepts discussed.
Gold challenges are difficult and can take hours to complete. They
require you to understand the concepts from the chapter and then do
some quality thinking and problem solving on your own. Tackling
these challenges will prepare you for the real-world work of React
development.
Before beginning a challenge, make a copy of your project and attack the
challenge in that copy. Many chapters build on previous chapters, and
working on challenges in a copy of the project ensures that you will be able
to progress through the book.
Hint: You will need to add new CSS in App.css and apply that class to your
new subheader.
Silver Challenge: Canarygram
Word about Ottergram has gotten out. The animal kingdom is clamoring for
photo-sharing applications! And no creature is more eager than the lovable
canary. Canaries have a reputation for being first for a lot of things, and
they are a little bummed that the otters beat them to the punch. Fear not,
canaries! From the makers of Ottergram comes the brand-new Canarygram.
At the moment, each element is hardcoded into App.js. Although this works,
and the elements render to the screen just fine, it does lead to a lot of repeated
code.
In this chapter, you will learn about creating and working with components.
By the end of the chapter, you will have moved the JSX markup from App.js
into separate component files. Using components will help you organize your
code, keep your code cleaner, and create dynamic, reusable functions. All of
this will make Ottergram easier to develop and maintain.
At the end of this chapter, your application will have one visual change: The
names of the otters will stand out more (Figure 2.1, “Ottergram with stand-
out names”).
When you look at, say, an airplane, it can be hard to wrap your mind around
all the different pieces it is made of. But an airplane does not start out as a
single unit. It is made from lots of individual parts, such as the wings, engine,
tail, and so on – each with its own purpose and functionality. As the airplane
is built and new parts are added, the pieces come together to form a new and
different whole.
In React, these pieces are called components, and they are a powerful tool.
Components are JavaScript functions that return React elements.
Header Component
Look at a wireframe of the Ottergram app at this point (Figure 2.2,
“Ottergram wireframe”).
In Visual Studio Code, create a new folder in the src directory called
components. To do this, select the src directory, then click the icon at the
top of the explorer (Figure 2.3, “Adding a new folder”).
By convention, a file usually contains only one component and shares the
name of the component it contains. Since you are creating a Header
component, you will name the file that contains it Header.js.
Inside the components folder, create a new file called Header.js. Creating a
new file is similar to creating a new folder: Select the components directory
you just created, then click the icon at the top of the explorer (Figure 2.5,
“Adding a new file”).
function Header() {
return (
<header className='header-component'>
<h1>Ottergram</h1>
</header>
)
}
After you define the function, you export it using export default. Exporting
the function allows other files to import and use it. (If you are wondering
why you define the function and export it separately, we will explain that
later in this chapter.)
Save Header.js. Now import the new Header component into App.js and
replace the existing header markup. You use the component inside angle
brackets, just like an HTML tag.
import './App.css';
import Header from './components/Header';
import Barry from './otters/otter1.jpg';
...
function App() {
return (
<div>
<header className='header-component'>
<h1>Ottergram</h1>
</header>
<Header />
<ul className='post-list'>
...
Save App.js and check out the browser. Though the app looks the same, you
are now looking at your first component. (If the page does not look the same,
you might not have saved one of the files.)
Even though you did not import the stylesheet directly into the new
Header.js file, your styles are still being applied. This is because all CSS
becomes global, so any CSS you create is available to your entire application.
You will often see separate stylesheets for each component. Although this
helps with organization and maintainability, it does not isolate the CSS to the
component.
What would your code look like without JSX? The Header component
would look like this:
Instead of having easy-to-read tags, this version of the Header function calls
the React API’s createElement function directly.
Both versions are valid code that returns a React element. But imagine
creating a component with more than a simple header, such as the App
component. Your code could quickly become messy and difficult to read.
JSX allows you to write code that is easy to read and understand.
Post Component
Take another look at Ottergram’s wireframe (Figure 2.7, “Wireframe
highlighting repeated elements”). Below the header, repeating elements
display each otter’s image and name.
To add another element, you have to add a new list item with its own button,
image, and text. This means you cannot reuse the code from previous items,
because you must specify the image and name of each item individually.
But because the underlying structure of each item is the same, you can extract
the structure into a single, reusable component. We will call this component a
“post,” since it will contain a single post on the website.
Create a new file in the components directory using the New File icon. Name
the file Post.js (Figure 2.8, “Creating Post.js”).
In Post.js, define a Post function and paste the list item code into the return
statement. Import the Barry image file. (If you copy the import statement
from App.js, you will need to add a . to the file path, because Post.js is
nested in the components directory.) Finally, add an export statement.
function Post() {
return (
<li className='post-component'>
<button>
<img src={Barry} alt='Barry'/>
<p>Barry</p>
</button>
</li>
)
}
Save Post.js and return to App.js. Import the Post component and replace
the Barry list item with a Post:
import './App.css';
import Header from './components/Header';
import Barry from './otters/otter1.jpg';
import Post from './components/Post';
import Robin from './otters/otter2.jpg';
...
function App() {
return (
<div>
<Header />
<ul className='post-list'>
<li className='post-component'>
<button>
<img src={Barry} alt='Barry'/>
<p>Barry</p>
</button>
</li>
<Post />
<li className='post-component'>
...
Save App.js and check out the browser. Although everything should look the
same, your app now includes two React components. Later in this chapter,
you will see how to reuse the Post component to simplify your code base.
But first, let’s take a moment to inspect your components using the React
Developer Tools.
React Developer Tools
The React Developer Tools, a Chrome browser extension, integrate with the
Chrome Developer Tools to show you additional useful information about
your components in the browser.
Install the extension to take a look. Visit the Chrome Web Store at
chrome.google.com/webstore/category/extensions. Search for “React
Developer Tools” (Figure 2.9, “Chrome Web Store”). On the extension’s
page, the Additional Information section should show that it is offered by
Meta.
In your Ottergram browser tab, make sure the Chrome DevTools are open. (If
they are not, click the triple-dot icon to the right of the address bar, then click
More Tools → Developer Tools.)
Because you have added the React DevTools, you will see two additional
tabs: Components and Profiler (Figure 2.10, “Opening the React DevTools”).
(You might need to click the >> icon in the tab bar to see them.) If you do not
see them, try restarting Chrome and checking again.
Select the Components tab. The React DevTools show your new Header and
Post components as part of the app structure (Figure 2.11, “Viewing
components in the React DevTools”).
The React DevTools also provide information about how your component is
being used in the application. The Post component, for example, is rendered
by the App component. App is considered the parent component of Post,
because it lives directly above it in the component tree. And the source
section shows the filename and line where your component is referenced.
If you are already familiar with JavaScript, you might be wondering why we
had you create and export your component in separate steps. For Header, for
example, an arrow function like this would have saved several lines of code:
If you defined Header like this, it would still render. But the React DevTools
would not be able to identify it by name, because the DevTools learn the
component name from the function name (Figure 2.13, “Anonymous
component name”).
Being able to see the component names in the DevTools is very helpful for
debugging, so we recommend creating and exporting your components in
separate steps. You will be glad you took the time to write those extra lines
later.
Props
Although your Post component is great, it is not doing much work yet. All
five list items in App.js have the same structure and should be able to use the
same component. But so far, you are using the Post component for only one
otter post, because the component’s image and text are specific to that one
otter.
How can you reuse the Post component for all five list items? The answer is
another React feature: props.
Props can be any type of data: JavaScript strings, numbers, objects, arrays,
functions, and so on. In JSX, you send props to components using attributes,
similar to HTML attributes. You group these attributes into the props object,
which you pass as the first parameter when calling the component function.
Post needs to know the image and name of the otter to display. These will be
the props that App passes down.
In App.js, use attributes to pass values for these two props to the Post
component. You will also need to import Barry’s image again.
import './App.css';
import Header from './components/Header';
import Post from './components/Post';
import Barry from './otters/otter1.jpg';
import Robin from './otters/otter2.jpg';
...
function App() {
return (
<div>
<Header />
<ul className='post-list'>
<Post />
<Post image={Barry} name='Barry' />
<li className='post-component'>
...
Now you are ready to receive the props in Post.js. React uses the image and
name attributes specified above as keys on the props object, allowing you to
access their values.
In Post.js, add props as the first parameter of the function. (It is standard
practice to name this parameter props.) Then, use dynamic values to replace
the hardcoded values in the component.
function Post() {
function Post(props) {
return (
<li className='post-component'>
<button>
<img src={Barry} alt='Barry'/>
<img src={props.image} alt={props.name}/>
<p>Barry</p>
<p>{props.name}</p>
</button>
...
You inserted JavaScript into the JSX markup using {}. This syntax is called a
JSX expression. JSX expressions are evaluated and then rendered along with
the other JSX.
Look in the DevTools again and select the Post component. Now you can see
details about the new props within the component (Figure 2.14, “Viewing
Post props in the DevTools”).
...
function App() {
return (
<div>
<Header />
<ul className='post-list'>
<Post image={Barry} name='Barry' />
<li className='post-component'>
<button>
<img src={Robin} alt='Robin'/>
<p>Robin</p>
</button>
</li>
<li className='post-component'>
<button>
<img src={Maurice} alt='Maurice'/>
<p>Maurice</p>
</button>
</li>
<li className='post-component'>
<button>
<img src={Lesley} alt='Lesley'/>
<p>Lesley</p>
</button>
</li>
<li className='post-component'>
<button>
<img src={Barbara} alt='Barbara'/>
<p>Barbara</p>
</button>
</li>
<Post image={Robin} name='Robin' />
<Post image={Maurice} name='Maurice' />
<Post image={Lesley} name='Lesley' />
<Post image={Barbara} name='Barbara'/>
</ul>
...
You can immediately see one benefit of this approach: The code is much
shorter and easier to read. Also, to change the way posts are structured or
how they appear on Ottergram, now you can make just one edit to the Post
component, instead of having to edit each list item individually.
You will see this for yourself in a moment. First, save your changes and
check your browser to make sure everything is working as before. Now you
will see the same charming otter posts, and the DevTools will show that you
are seeing them in a series of Post components (Figure 2.15, “Multiple
components”).
In your stylesheet, App.css, add a new style rule for the class post-name to
make the text larger and bold.
...
.selected-item p {
display: flex;
justify-content: center;
font-size: 2.5em;
}
.post-component .post-name {
font-weight: bold;
font-size: 2em;
}
function Post(props) {
return (
<li className='post-component'>
<button>
<img src={props.image} alt={props.name}/>
<p>{props.name}</p>
<p className="post-name">{props.name}</p>
</button>
...
Save your files and return to your browser. Check out the names of each otter
(Figure 2.16, “Bold otter names”).
Object destructuring
As it is written, the props object does not give you a lot of insight into what
data the component expects. Because the props argument is a JavaScript
object, you can destructure it to get the individual props.
function Post(props) {
function Post({ image, name }) {
return (
<li className='post-component'>
<button>
<img src={props.image} alt={props.name}/>
<img src={image} alt={name}/>
<p className="post-name">{props.name}</p>
<p className="post-name">{name}</p>
</button>
...
Save your file and check Ottergram in the browser to confirm that everything
still looks the same.
Rendering Lists
The Post component has streamlined your list item code in App.js. But
adding posts one at a time is still somewhat tedious. And if you were working
with more than a small list of otters, adding each post could easily become an
impossible task.
In a real-world app, the items you add to a list will probably be related to
each other in some way. The data will likely be grouped in an array or
another structure, and it might come from an external source or from a data
set within your app.
And when you want to render data from a list of individual components,
React has tools to help you.
To see how this works, begin modeling a more realistic setup by creating an
array with the data you currently have. (Although it would be most true to
life for your data to come from an external source or a separate data file, go
ahead and hardcode your array in App.js.)
...
import Barbara from './otters/otter5.jpg';
const ottersArray = [
{ image: Barry, name: 'Barry' },
{ image: Robin, name: 'Robin' },
{ image: Maurice, name: 'Maurice' },
{ image: Lesley, name: 'Lesley' },
{ image: Barbara, name: 'Barbara' },
];
function App() {
...
Now you can replace the repetitive code that creates the Post components.
Use {} to insert JavaScript into your JSX markup. Within the JavaScript
braces, use the Array.map function to iterate over the items in the array and
return a Post component for each one.
...
function App() {
return (
<div>
<Header />
<ul className='post-list'>
<Post image={Barry} name='Barry' />
<Post image={Robin} name='Robin' />
<Post image={Maurice} name='Maurice' />
<Post image={Lesley} name='Lesley' />
<Post image={Barbara} name='Barbara'/>
{ottersArray.map((post) => (
<Post
image={post.image}
name={post.name}
/>
))}
</ul>
...
Why break down each post into image and name and pass those as separate
props?
The Post component is primarily about presentation. Although the post item
contains only two properties right now, eventually you might decide to add
more. It is best not to couple components to the format of other data, because
doing so limits their reusability. Instead, pass Post only the information it
needs to render.
Keys
Save your file and switch to the browser. Though Ottergram looks the same,
there is a problem.
Using the >> in the DevTools menu bar, open the Console tab. At the bottom
of the console, you will see a red warning (Figure 2.17, “Key warning in the
browser”).
What happened? As the warning says, you should include a key as a prop for
each child in the list.
A key is a value that uniquely identifies a component. React uses keys so that
when an item in a list is added, removed, or changed, it can render only that
item and not the full list.
Each key must be unique among its siblings and must be consistently
associated with the same piece of data. Using the same key more than once in
an array results in a duplicate key warning, similar to the warning you see
here, and might have unintended side effects.
You cannot insert random values as the keys. Although random values might
be unique, they can change each time React re-renders the components,
negating any built-in optimizations.
Similarly, the array index is not a good value to use for the key because it can
change. For example, when you remove an item from the front of an array,
the indexes of the following items shift by one. This means the array index is
not guaranteed to consistently identify the same component.
Often, databases are built so that each element has a key or ID, which you
can use as the component key in React. To simulate this, add an ID to each
item in the otters array. Then, use the ID to add a key to each of the Post
components:
...
const ottersArray = [
{ image: Barry, name: 'Barry' },
{ image: Barry, name: 'Barry', id: 1 },
{ image: Robin, name: 'Robin' },
{ image: Robin, name: 'Robin', id: 2 },
{ image: Maurice, name: 'Maurice' },
{ image: Maurice, name: 'Maurice', id: 3 },
{ image: Lesley, name: 'Lesley' },
{ image: Lesley, name: 'Lesley', id: 4 },
{ image: Barbara, name: 'Barbara' },
{ image: Barbara, name: 'Barbara', id: 5 },
];
function App() {
return (
...
{ottersArray.map((post) => (
<Post
key={post.id}
image={post.image}
...
Save your file and head back to the browser. Reload the page; the red
warning should disappear from the console.
Use the >> in the DevTools menu bar to switch to the Components tab. The
new keys appear next to each component (Figure 2.18, “Keys in the
Components tab”).
In the next chapter, you will continue to build on what you have learned and
give users the ability to interact with your app by highlighting their favorite
otter.
For the More Curious: Testing Prop Changes in the
DevTools
The React DevTools allow you to try out changes to props and immediately
see the results in your app. This can be a helpful tool when working with
dynamic data.
In the props section of the Components tab, select the value of the prop you
want to edit and replace it with a new value (Figure 2.20, “Editing the name
prop in the DevTools”).
First, update your Canarygram code base to use organized components, like
you did with Ottergram.
Next, add a footer to Canarygram. The canaries heard about the new footer
feature, but they want their own spin on it. The footer should have links to
the Wikipedia pages for two different types of canaries:
en.wikipedia.org/wiki/Atlantic_canary
en.wikipedia.org/wiki/Yellow_canary
Chapter 3. User Events
Ottergram now displays otters’ images and names, and that is great.
However, users expect more from a modern application. Users want to be
able to interact with your website. For example, it would be nice if users
could click one of the post items to showcase a special otter.
In this chapter, you will implement this functionality using an event handler.
Event handlers let React know what to do when a user event, such as a button
click, occurs. Handling events in React is similar to handling events in
standard HTML and JavaScript – with a few differences, which we will
discuss along the way.
By the end of this chapter, you will be able to select different posts and see in
the Console tab that you have clicked them. And you will have built a new
component to display the featured post, like Figure 3.1, “Barry takes center
stage”:
In the next chapter, you will finish your work on Ottergram by allowing the
user to click any otter post to feature it.
Adding SelectedItem
Before adding an event handler, you will build a new SelectedItem
component to display the featured otter.
You will build SelectedItem the same way that you built Header and Post.
Start by creating a new file in the components directory called
SelectedItem.js (Figure 3.2, “New SelectedItem.js file”).
...
function App() {
return (
<div>
<Header />
<div className='app-content'>
<ul className='post-list'>
...
</ul>
</div>
</div>
...
(To fix the indentation of the lines between the new <div>’s opening and
closing tags, you can take advantage of a Visual Studio Code shortcut:
Highlight all the lines you want to indent and press the Tab key.)
Now you are ready to add your SelectedItem component to App.js. Import
the new component at the top of the file and add the component tag directly
under the closing tag for the <ul>.
For now, use the first item in the otter array as the featured otter by passing
along its image and name.
...
import Post from './components/Post';
import SelectedItem from './components/SelectedItem';
import Barry from './otters/otter1.jpg';
...
function App() {
return (
...
<ul className='post-list'>
...
</ul>
<SelectedItem
image={ottersArray[0].image}
name={ottersArray[0].name}
/>
</div>
...
Save your files and check out the browser. Now Ottergram features a special
otter (Figure 3.3, “Viewing the selected otter in the browser”).
React still does not know what to do when the user clicks an item. Let’s start
fixing that by adding an event handler to the Post component.
User Events
Your component can listen for many user events, including pressing a key,
moving the mouse, or submitting a form. You will listen for a click event –
specifically, when the user clicks the button in Post.
For now, keep the function simple. Return a console.log statement that
prints the event.
return (
...
The next step is to listen for the user’s click and fire the handleClick
function.
React event listeners are very similar to event listeners in vanilla JavaScript:
Both give you access to the event details. In handleClick, you can see that
the function takes the event as a parameter.
One difference is that React uses camelCase for naming events. For example,
the JavaScript onclick becomes onClick in React.
Add an onClick attribute to the <button> tag and set it equal to the
handleClick prop. This will bind the function you created to the user event
click on the button.
Example 3.5. Setting the onClick attribute equal to the new function
(Post.js)
Save your files. In your browser, open the DevTools and select the Console
tab. (If you do not see the Console tab, click >> in the menu bar to find it.)
Click any of the otter posts. A log statement appears in the console each time
you click (Figure 3.4, “Viewing console logs in the browser”).
In React, you often need to pass parameters to the event handler. Therefore,
although handleClick can already access the otter name from the props, you
will pass name to it as a parameter. In Post.js, update handleClick to accept
otterName as a parameter, then log this name to the console. In the onClick
handler, pass name to the handleClick function.
(This code change will produce unexpected results. You will fix it in the next
section.)
return (
<li className='post-component'>
<button onClick={handleClick}>
<button onClick={handleClick(name)}>
<img src={image} alt={name}/>
...
Save your file, refresh your browser, and check out the Console tab
(Figure 3.5, “All otter names in the console”).
This is a common mistake developers make in React. To fix it, you need to
pass a function. You can do this by creating an anonymous function inline.
Then, you bind the new function to onClick so it executes only when the user
clicks the button.
Refactor your code to use an anonymous arrow function.
Save your file and refresh your browser. Watch the console as you click a few
posts. Now the console logs an otter’s name only when you click that otter’s
post (Figure 3.6, “Otter names in the console”).
You added a new component that shows off a selected otter, and you
implemented an onClick event handler that fires every time a user clicks on
the button element.
There is still one more step: When the user clicks a new otter, you need to
update the otter displayed in the SelectedItem component. In the next
chapter, you will implement state in React and get started with React hooks
to tackle this issue.
For the More Curious: Alternate file-naming
conventions
You have been naming your component files using PascalCase. This is the
most common component filename convention, and it is the style that the
Create React App template uses. However, React allows you to find a style
that works best for your project and team. You can also use camelCase,
snake_case, or kebab-case.
Also, you are using the .js extension for all files that use JavaScript. Out in
the wild, you might also see the .jsx extension for files that contain JSX.
Although this is not required, it is useful if you want to designate which
files contain JSX.
For the More Curious: Detaching Event Handlers
In JavaScript, you might have come across code that looks like this:
EventTarget.addEventListener()
EventTarget.removeEventListener()
Letting React worry about this for you saves you a lot of effort and ensures
that there are no issues resulting from event handlers that you attached but
never detached.
Bronze Challenge: Hello, Goodbye
As we mentioned earlier, React provides many event handlers for your
components. For example, there are event handlers that can tell you when a
user hovers their mouse over one of your components and when they move
away.
Use these events to log Hello, {otterName} when the mouse enters a Post
component and Goodbye, {otterName} when the mouse leaves that
component.
Hint: You will want to use the onMouseEnter and onMouseLeave events,
which work similarly to onClick.
Although the selected post feature is not quite finished, the canaries want
their app in the hands of their alpha users right away. Simple logging is not
enough for them. Instead, when a user clicks a post, show an alert, using
so they can see that they have clicked the post without having to open the
DevTools console.
Chapter 4. State
While it is great that Barry the otter is getting some time to shine, all the
otters should have equal opportunities for fame. Your app currently listens for
user events and responds by logging the name of the otter that the user clicks.
In this chapter, you will finish up Ottergram by adding state to keep track of
and display the featured otter.
This chapter introduces React hooks, a powerful tool for managing state in a
component. You will continue to learn about hooks throughout this book, so
do not worry about mastering them right away. In this chapter, you will focus
on using the useState hook to manage a component’s state.
React ensures that what you return from your component matches what
renders onscreen. When a component’s state changes, it triggers React to re-
render the component and its children, updating the UI to match the new data.
We say that React is declarative because you declare the new state you want
to display onscreen, and React handles the job of getting it there.
useState
The tool you use to access a component’s state is a React hook called useState.
Until React 16.8 introduced React hooks, all components with internal state had to be written as
classes. An advantage of class components is that they do not automatically re-render when their
parent component re-renders, the way functional components do. However, class components are
generally more complex and contain more boilerplate code than functional components.
Hooks provide functional components (such as the ones you have been creating) with access to
state and other React features, largely eliminating the need for complex classes. You can read
more about functional and class components in the section called “For the More Curious:
Components – Functions vs Classes” near the end of this chapter. And there are additional tools
you can use to protect against automatic re-renders in functional components, which you will
learn about in Chapter 19, Introduction to App Performance Optimization.
At their core, hooks are JavaScript functions that accept arguments and return specific values.
The useState hook takes one argument: the initial state value. It returns an array with two
values: the current state value and a function for updating the state. (Incidentally, all hooks in
React have names that begin with use – this makes them easy to identify.)
Add a piece of state called selectedPostName using the useState hook. Later, you will use
selectedPostName with the ottersArray to find the currently selected otter. For now, set the
initial value of the state to Barry to select Barry the otter.
return (
...
You import useState from React so that you can use it in the component. You must always
import React hooks before you can use them.
Then you use array destructuring to get two items from the returned array: the current state value
and the function that updates it. Without array destructuring, you would need several lines of
code:
Though you can name the variables created from the array whatever you want, it is a best
practice for the name of the update function to start with the word set, followed by the state
name. This makes it easy to see that the state and its update function are related.
Save your file, make sure your development server is running, and switch to the browser. Open
the Components tab in the React DevTools and inspect the App component. You now have new
information available to you about the component: You can see the component’s state in the
hooks section (Figure 4.2, “Viewing state in the React DevTools”).
A component can pass down a state value as a prop to another component. As the next step
toward displaying the selected otter, update the props for SelectedItem to use the state.
Example 4.2. Using selectedPostName to show the featured otter (App.js)
...
function App() {
const [selectedPostName, setSelectedPostName] = useState('Barry');
const selectedPost = ottersArray.find(otter => otter.name === selectedPostName);
return (
...
</ul>
<SelectedItem
image={ottersArray[0].image}
image={selectedPost.image}
name={ottersArray[0].name}
name={selectedPost.name}
/>
...
Here, you add a new variable: selectedPost. Since the state contains only the name of the otter,
you use the Array.find method to find the array item with a matching name. You then use
selectedPost to pass along the correct props.
However, because the value of selectedPostName is currently hardcoded as Barry, Barry will
remain the featured otter until the state updates.
Updating State
Recall that useState returns your update function, setSelectedPostName.
You will use this function to update the state value to a new ID when the
user clicks a Post component. setSelectedPostName will take one
argument, the updated state value, and use it to replace the existing state
value.
But there is a problem: Although the click handler lives in the Post
component, the state and the function to update it live in App.js.
No worries!
You can also pass functions as props. You will pass setSelectedPostName
as a prop to the Post component. The Post component can then call
setSelectedPostName each time the user clicks a post.
...
function App() {
...
return (
...
<ul className='post-list'>
...
image={post.image}
name={post.name}
setSelectedPostName={setSelectedPostName}
/>
...
return (
<li className='post-component'>
<button onClick={() => handleClick(name)}>
<button onClick={() => setSelectedPostName(name)}>
<img src={image} alt={name}/>
...
Save your files and return to the browser. Click different Post components.
The selected otter updates each time you click a new Post.
Conclusion
Congratulations! Your first React application is complete. You built React
components and used props and state to display data in Ottergram. You will
continue to build on what you have learned in your next application.
As we have said, writing a component as a class is still valid React code. This is how
Ottergram’s Post component would look as a class component.
handleClick() {
this.props.setSelectedPostName(this.props.name);
}
render() {
return (
<li className='post'>
<button onClick={this.handleClick}>
<img src={this.props.image} alt={this.props.name} />
<p>{this.props.name}</p>
</button>
</li>
)
}
}
Class components extend React.Component and return JSX inside a method called
render. Although the render method is the only required method for class components,
you can also add other actions as methods.
In this version of the Post component, the event handler, handleClick, is a class method
rather than an inline handler. Inline click handlers are much more common in functional
components than class components, where they are frequently class methods.
Class components also have access to several other built-in methods. These include
component lifecycle methods, such as componentDidMount, componentDidUpdate, and
componentWillUnmount, which developers use to control component behavior during the
React render cycle. Although functional components do not have access to these lifecycle
methods, you can achieve similar behavior using hooks.
Notice the use of the keyword this in this class-based version of Post. this is a special,
reserved word in JavaScript, used to contain contextual data.
In class components, this often refers to the value of the class object. However, functions
or methods inside the class, including the constructor, can return different values of this,
depending on the scope.
In this class-based Post component, this.props gives you access to the component’s
props. Similarly, this.state returns the current value of the component’s state. (In this
example, the state value is null.)
You must bind the handleClick method to the class in the constructor. handleClick
accesses the value of this inside the function when calling
this.props.setSelectedPostName. Binding the method ties the value of this to the
class object, rather than to the method itself. Otherwise, this.props would be undefined
because the method does not have props.
Although you can also access this in functional components, you would almost never use
it. In functions, the value of this depends on how you call the function and whether you
enable strict mode.
this has always been confusing to learn and talk about. Its complexity has led to many
difficult-to-find bugs. Thankfully, functional components use this only sparingly. We
recommend that you minimize your use of this to avoid unintended behavior.
Gold Challenge: Canary Shuffle
Note: You can work on this challenge in a copy of Ottergram if you have
not been building Canarygram.
The canaries have asked for one last feature: Each of them would like a
chance to be the first post displayed in the list of posts.
Add a Shuffle button that will reorder the canaries’ posts. The first post
should move to the last spot, and the other posts should move up one spot.
In addition to creating another <button> and click event handler, you will
need to create a new state value in App.js that holds the canariesArray.
Hint: For React to pick up changes to the state value, you will need to pass
a new array to the setState function instead of mutating the original array.
One way to create a new array is with the spread operator (...).
If you get stuck, you can ask questions in the forum for this book at
forums.bignerdranch.com.
Chapter 5. Linting
In this chapter, you will set up the Code Café application, which you will
build over the next 15 chapters.
Code Café will be an e-commerce site that features food and drinks for sale.
When the application is complete, users will be able to add items to their
carts, edit their carts, check out, and submit their orders. You will also add
authentication to allow users to log in to the site.
You already have experience setting up a React project, so this chapter will
give you the opportunity to practice what you have learned.
In addition to the basic setup process you saw with Ottergram, you will set up
JavaScript linting for Code Café. A linter is a tool that helps enforce code
styles and detect potential errors. You will frequently see developers use
linting in React production code. The linting rules you set up here will
familiarize you with what linters can offer and help you troubleshoot when
you run into warnings or errors in the future.
By the end of this chapter, you will have a new React project that you will be
able to see running in the browser. You will also have installed a linting tool
and fixed any code errors it has found.
Creating a Project
Begin by creating a new project using Create React App. In your terminal,
open a new tab or window and navigate to your react-book projects folder.
Use the create-react-app tool to create a new React application called
code-cafe:
When the script is complete and you see the success message in your
terminal, navigate to the new project. Remember that Create React App
creates your application in a subdirectory named for your project title, so
change your terminal to that directory with the cd command:
cd code-cafe
npm start
The tool might open the new application in a browser window for you. If it
does not, open a new Chrome window and go to http://localhost:3000 to view
the default React application (Figure 5.2, “Create React App”).
You will follow the same process to update these items that you used for
Ottergram, so feel free to try it on your own before looking at the steps and
code below. (The favicon file is provided for you in your downloaded
resources file.) Remember that you might need to manually refresh your
browser to see the updated title and favicon.
In Visual Studio Code, open the code-cafe project folder by selecting File
→ Open Folder… in the top navigation bar and then choosing code-cafe
from your react-book projects folder (Figure 5.3, “Opening the Code Café
folder”).
<!DOCTYPE html>
<html lang="en">
<head>
...
<!--
Notice the use of %PUBLIC_URL% in the tags above.
...
-->
<title>React App</title>
<title>Code Café</title>
</head>
...
Now users will see the correct title when they navigate to your site
(Figure 5.5, “Title on the browser tab”).
All the icons you will need for Code Café, including the favicon, are in your
downloaded resources file.
Good – you have customized your site info so users can find Code Café.
Linting Overview
Before you jump into building Code Café, you will configure ESLint and
add linting rules to your application.
Most React projects and teams you work on will have a set of linting rules
developers must adhere to. A linter provides two main benefits:
ESLint has hundreds of rules you can enable. For example, ESLint can
perform inspections for unused or missing variables, which can indicate
misspelled words or incorrect import statements that will lead to errors.
ESLint can also enforce the use of semicolons in code. Look at the function
below:
At first glance, it appears that the function sum returns the value of 2 + 1,
which is 3. However, because of JavaScript’s automatic semicolon
insertion, this code actually evaluates as
With the addition of the semicolon after the return keyword, sum returns
undefined and never evaluates the remaining code.
With the semi rule enabled, ESLint will point out the missing semicolon
after the return keyword, alerting you to a potential problem. You can then
rewrite the function to work as intended:
So, do you need to sort through the hundreds of available rules to find the
ones you want to apply? Thankfully, no. There are many preconfigured rule
sets available to save you time and effort. You will see how this works in a
moment.
Open a new terminal window and navigate to the code-cafe directory. Run
the following command to see the ESLint configurations included with
Create React App:
npm ls eslint
└─┬ [email protected]
├─┬ [email protected]
│ ├─┬ @babel/[email protected]
│ │ └── [email protected] deduped
│ ├─┬ @typescript-eslint/[email protected]
│ │ ├─┬ @typescript-eslint/[email protected]
│ │ │ └── [email protected] deduped
│ │ ├─┬ @typescript-eslint/[email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected] deduped
│ ├─┬ @typescript-eslint/[email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ ├─┬ @typescript-eslint/[email protected]
│ │ │ └── [email protected] deduped
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └
│ │ └── [email protected] deduped
│ ├─┬ [email protected]
│ │ └── [email protected] deduped
│ └── [email protected] deduped
├─┬ [email protected]
│ └── [email protected] deduped
├─┬ [email protected]
│ └─┬ [email protected]
│ └── [email protected] deduped
└─┬ [email protected]
└─┬ [email protected]
└── [email protected] deduped
At its base, ESLint provides rules for writing JavaScript code. You can add
rules via plugins, which are special files that add rules not included in the
default eslint package.
In addition to this configuration, you will use a popular set of ESLint rules
put together by Airbnb. Airbnb’s rule set includes rules for React and
JavaScript, such as the semi rule from the example above. Though this rule
set is opinionated, the preset lint rules will eliminate formatting
discrepancies and result in code that is easy to read.
There is no need to install additional plugins, because Airbnb’s rule set uses
the same ones included with Create React App.
As you work through Code Café, using this version will ensure consistency
between your code and what you see in the book. Generally, in your own
development work, you will want to install the most recent versions of
packages, using just the package name without a version number.
When the installation is complete, you might see output like this warning
about vulnerabilities:
npm audit runs behind the scenes each time npm installs a new package.
Although you should never ignore security warnings, npm audit often flags
items that are not applicable to your project or are false positives. In this
case, because the code is running on only your machine and you are the
only one accessing the website, you can continue without further action.
If you are concerned and want to investigate further, run the npm audit
command again locally to list the security vulnerabilities that npm
identified when it installed the package.
npm audit also gives you the option to fix any problems with the command
npm audit fix --force. Running this command will likely force updates
to other dependencies in your project, causing new vulnerabilities or errors
that will make it difficult to follow along in the book. If you choose to run
this command, we recommend that you first make a backup of your project
by copying the files or using Git, so you can revert if needed.
Next, open package.json in Visual Studio Code. The package you just
installed is included under dependencies:
{
"name": "code-cafe",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"eslint-config-airbnb": "^19.0.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
...
}
Although you have added the Airbnb rule set as a dependency to Code
Café, you still need to configure it so you can use it in your project.
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
The two configuration files that are extended here, react-app and react-
app/jest, relate to the eslint-config-react-app rule set that Create
React App added. Although react-app/jest is included with eslint-
config-react-app, you must explicitly extend it to use it.
...
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app/jest",
"airbnb",
"airbnb/hooks"
]
},
...
By default, Create React App does not include a script for running the linter. To run it manually,
target the project ESLint file by executing the following command in your terminal:
In addition to specifying the eslint path, this script adds two parameters, src and max-warnings.
src specifies that ESLint should check files in only the src directory. (Remember, ESLint is a
JavaScript linter.)
As for max-warnings, ESLint has two levels of checks: warnings and errors. By default, if ESLint
finds any errors, it will exit with an error status. If there are warnings but no errors, the lint check
will resolve successfully.
Adding the max-warnings option forces ESLint to exit with an error status if the number of warnings
exceeds the value you set. Because you set max-warnings to 0, ESLint will exit with an error status
if it finds any warnings or errors. Though engineering teams each handle warnings differently, it is
common to limit the number of lint warnings in production code.
Typing out this entire command every time you want to run the linter would be cumbersome, so you
will add a script to make life simpler.
In package.json, find the scripts section and add a new script called lint to run eslint.
...
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"lint": "eslint src --max-warnings=0",
"eject": "react-scripts eject"
},
...
Because react-scripts includes eslint, you do not need to specify the file path in this script.
Now you can use the command npm run lint in the terminal to run ESLint and check your code
style. Try it out.
Take a look at the errors in your terminal. Your output might be slightly different, depending on your
version of Create React App. (Also, we have broken each line of output to fit on the printed page.)
/.../code-cafe/src/App.js
6:5 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
6:5 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
7:7 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
8:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
9:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
10:16 error `code` must be placed on a new line
react/jsx-one-expression-per-line
10:16 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
10:39 error ` and save to reload. ` must be placed on a new line
react/jsx-one-expression-per-line
12:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
/.../code-cafe/src/App.test.js
5:10 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
5:10 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
/.../code-cafe/src/index.js
9:3 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
11:22 error Missing trailing comma
comma-dangle
/.../code-cafe/src/reportWebVitals.js
1:25 error Expected parentheses around arrow function argument
arrow-parens
3:32 error Expected a line break after this opening brace
object-curly-newline
3:74 error Expected a line break before this closing brace
object-curly-newline
The problem is that Create React App does not adhere fully to the lint rules from Airbnb that you
configured. ESLint has identified 16 problems in the code and listed them by file.
Several of the errors that ESLint identified are related to just two rules: react/react-in-jsx-scope
and react/jsx-filename-extension. In a moment, you will learn why these rules exist and how to
modify them. For now, ignoring those two rules leaves six other errors to review.
The ESLint output shows the file and line number where the error occurs. Open src/App.js. ESLint
identified two errors on line 10 in this file, both related to the rule react/jsx-one-expression-per-
line.
...
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
Edit
{' '}
<code>src/App.js</code>
{' '}
and save to reload.
</p>
...
JSX removes whitespace between lines. To make the text render correctly, you add lines of code
with strings containing a space. This is a workaround you will see out in the wild.
Run the lint command again (npm run lint). The errors related to react/jsx-one-expression-
per-line disappear from src/App.js:
/.../code-cafe/src/App.js
6:5 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
6:5 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
7:7 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
8:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
9:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
10:16 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
12:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
/.../code-cafe/src/App.test.js
5:10 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
5:10 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
/.../code-cafe/src/index.js
9:3 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
11:22 error Missing trailing comma
comma-dangle
/.../code-cafe/src/reportWebVitals.js
1:25 error Expected parentheses around arrow function argument
arrow-parens
3:32 error Expected a line break after this opening brace
object-curly-newline
3:74 error Expected a line break before this closing brace
object-curly-newline
Now open src/index.js and notice that line 11 is missing a trailing comma – which the comma-
dangle rule requires, as the output above indicates. Do not fix this error yet. Instead, open
src/reportWebVitals.js and review lines 1 and 3 for the errors indicated in the output.
(If you are not sure why any of the lines flagged in the output violates a rule, you can find more
information on the rules in the ESLint documentation: eslint.org/docs/latest/rules.)
Auto-fixing errors
Many ESLint rules can go beyond simply flagging violations and actually reformat the code to be
valid. This is called auto-fixing, and you trigger it by passing ESLint the --fix flag.
ESLint tells you how many errors and warnings are potentially fixable using --fix. In this case,
ESLint can auto-fix all four errors you just reviewed.
Use npm to run the lint command and fix the errors, like this:
What is the extra -- for? It tells npm to pass all the arguments that follow it to the command being
executed. In this case, --fix is a parameter that npm passes to the eslint command.
Using the --fix argument fixes the four errors you just reviewed. Open src/index.js to verify that
ESLint added a comma to line 11. ESLint also modified src/reportWebVitals.js, adding
parentheses around the function argument in line 1, and adding line breaks further down in the file.
Why bother running npm run lint to see the errors and warnings? Why not just run npm run lint
-- --fix to make fixable errors go away? Sometimes, auto-fixing can change code that you do not
want to change. Always look over the list of errors before auto-fixing them.
After ESLint makes those fixes, the output tells you there are still 10 errors remaining.
/.../code-cafe/src/App.js
6:5 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
6:5 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
7:7 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
8:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
9:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
12:11 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
16:9 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
/.../code-cafe/src/App.test.js
5:10 error 'React' must be in scope when using JSX
react/react-in-jsx-scope
5:10 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
/.../code-cafe/src/index.js
9:3 error JSX not allowed in files with extension '.js'
react/jsx-filename-extension
The remaining errors that ESLint has identified are related to just two rules: react/react-in-jsx-
scope and react/jsx-filename-extension.
What happens if you disagree with an ESLint rule? You can modify ESLint rules for specific chunks
of code or for entire files.
To disable a rule for one line of code, add a comment directly above the line with this syntax:
/* eslint-disable-next-line [rule-name] */
You can put a similar comment at the top of a file to disable a rule for the entire file:
/* eslint-disable [rule-name] */
Both of these options exempt a particular section of code, leaving the rule in place. (Other options
for modifying ESLint rules are also available. For more information, see the documentation at
eslint.org/docs/latest/user-guide/configuring/rules.)
Alternatively, ESLint allows you to override default rules or to set your own rules at the application
level by adding a rules section inside eslintConfig.
Which brings us back to the two ESLint rules that are the source of all your remaining errors:
react/react-in-jsx-scope and react/jsx-filename-extension.
The first states that “React must be in scope when using JSX.” In previous versions of React, you
had to import React into any file returning JSX. The react/react-in-jsx-scope rule flags any files
that contain JSX but do not have the React import. Recent versions of React have changed how JSX
compiles and made this rule obsolete, so you will turn it off.
The second error, react/jsx-filename-extension, states: “JSX not allowed in files with extension
.js.” Airbnb’s default setting for this rule requires that any file containing JSX end with the .jsx
extension. However, Create React App generates files with the .js extension, and you will follow
that convention for the files you create in this book. You will override this rule as well, so that you
can include JSX markup in files with either extension.
...
"eslintConfig": {
"extends": [
...
]
],
"rules": {
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [
"warn",
{
"extensions": [
".js",
".jsx"
]
}
]
}
},
...
Rules are listed as key-value pairs, where the key is the name of the rule, and the value is the rule
setting. If there are additional options, as is the case with the react/jsx-filename-extension rule,
the value is an array, where the first item is the rule setting, followed by any options.
You turn the react/react-in-jsx-scope rule off, because it is obsolete. But instead of turning the
react/jsx-filename-extension rule completely off, you set it to warn you if a file containing JSX
does not end in either the .jsx or .js file extension.
Save the file and check for lint errors again with npm run lint. Now there are no errors or
warnings. Doesn’t that feel good?
Before moving on, you need to override one more rule. You will turn off a rule called no-console,
which flags any console.log statements that you include in your code.
In a production environment, teams often disable logs to the console. Instead, they use a separate
logging package, such as Sentry, to store and search through logs in the cloud. Since Code Café will
be running on only your machine, you will not use a logging package. Instead, you will use
console.log statements for debugging. Turning the no-console rule off allows you to use
console.log statements without linting errors.
...
"rules": {
"react/react-in-jsx-scope": "off",
"react/jsx-filename-extension": [
"warn",
{
...
}
]
],
"no-console": "off"
}
...
The no-console rule does not use the react prefix, unlike the other rules, because it comes from the
eslint rule set and not from a plugin.
Installing the ESLint Extension in Visual Studio
Code
Running scripts in the terminal to find and fix potential code errors is helpful.
But wouldn’t it be nice to see errors in real time as you type? Good news:
You can, with the ESLint extension in Visual Studio Code.
A small red squiggle appears where the semicolon should be. Hover over the
marker to see a pop-up with information from ESLint (Figure 5.8, “ESLint
missing semicolon warning”).
You should also see a problem indicator in the bottom left of the Visual
Studio Code window (Figure 5.9, “One problem”).
Click the error icon to open the problems pane, which shows information
about all the errors and warnings found (Figure 5.10, “Problems pane”).
Now to fix that pesky semicolon error. The ESLint extension allows you to
auto-fix all the errors and warnings in the document from the actions menu.
Save your freshly fixed file. You can close the problems pane by clicking the
X in its top-right corner.
Conclusion
Having ESLint configured in your application will help you identify and fix
potential problems as you build your app. In the next chapter, you will get
practice building components as you lay the foundation for Code Café.
For the More Curious: Shortcuts to Running
ESLint in Visual Studio Code
Running the ESLint extension from the actions menu is a little tedious. There
are a couple ways to streamline auto-fixing your files: creating a keyboard
shortcut and setting Visual Studio Code to auto-fix whenever you save files.
Using these methods, you will probably perform lint checks more frequently,
allowing you to fix problems as they arise instead of having a bunch of issues
to deal with at the end of the development process.
Bear in mind that if you want to review errors and warnings before they are
fixed, you must review them before using either of these auto-fixing
methods.
Note that Visual Studio Code supports chords – two separate keystroke
combinations entered in sequence, such as Command-Shift-A Command-
Shift-A. If you enter one combination after another, the second combination
will be appended to the first to make a chord. If you do not want the chord,
enter the new combination again to set it as the single-combination shortcut.
The settings window has two tabs: User and Workspace. Settings on the User
tab apply globally; those on the Workspace tab apply only to the current
workspace, or project.
In the Workspace tab, begin typing “code actions on save” and click Edit in
settings.json when it appears.
Ignore these suggestions and add the code below instead. (If you do use the
suggestion to add source.fixAll, you will need to modify the value to
include .eslint and ensure it is set to true.)
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
},
"eslint.validate": [
"javascript"
]
}
Although linting the whole project works for small projects, it can be
painfully slow for larger projects. Another npm package, lint-staged, runs
ESLint on only the files that are staged to be committed. This makes the
pre-commit hook much faster. You can find out more about lint-staged at
www.npmjs.com/package/lint-staged.
Chapter 6. Prop Types
Now that your application is set up, you are ready to begin building Code
Café.
To make components easier to work with, you can assign prop types to your
component props. A prop type defines the data type, such as string or
number, required for a given prop. (If you have experience with typed
function parameters in TypeScript or other languages, prop types will likely
feel familiar.)
Prop types serve as documentation to help future developers know how to use
your component. They also give you some automatic checks for
compatibility between the type required for a component’s prop and the
actual prop being passed in, as you will see in a moment.
By the end of this chapter, Code Café will have three components: a Header
component proudly proclaiming the name of the site, a Home component that
renders when a user first navigates to your site, and a Thumbnail component
you will use multiple times to display a list of items available for purchase
(Figure 6.1, “Code Café at the end of this chapter”). And you will use the
prop-types library to document all the props your components use.
When building each new component, try reading through the instructions and
writing the code on your own. Then compare your code with ours.
It is OK if you still have questions about components when you reach the end
of this chapter. Building components is a fundamental skill, and you will
have more opportunities to practice as you continue to work on Code Café.
Adding Resources
You will need some images and other resources for your components. They
are in the resources file you downloaded in The Necessary Tools.
Make a place for these resources by setting up a new items folder in the src
directory: In Visual Studio Code, select src and click the New Folder icon at
the top of the explorer (Figure 6.2, “Adding a folder”).
Next, create an images folder in the src directory and copy all the files from
code-cafe-resources/images to src/images.
Finally, create a src/components folder. This is where you will house the
components you build.
Header Component
As in Ottergram, your first component for Code Café will be its header.
Ottergram’s header component was simple – just the app’s name, with some styling. Code Café’s
header component will start off with the app’s name and an image of a coffee cup. Later, you will
make it more complex.
In the new file, create and export a function that returns the header image and text. The coffee cup
image is in the images folder you just created: src/images/logo.svg.
(It is not unusual to encounter ESLint errors about line breaks or indentation in your file. Do not
panic! We will discuss these issues in a moment.)
Once you have written your header component, compare it with the code shown below:
function Header() {
return (
<header>
<img src={CoffeeLogo} alt="coffee logo" />
<h1>Code Café</h1>
</header>
);
}
Whitespace issues
You might have gotten some ESLint errors in your new Header file. Let’s go over three common
issues and how to fix them. First is this error:
By default, Windows uses two characters for a new line: a carriage return (CR) plus a line feed (LF).
Unix systems use just an LF and might display an extra blank line when both CR and LF are used.
(On some old printers, a carriage return moved the printhead to the left margin, without advancing
the paper, while a line feed advanced the paper one line.)
Visual Studio Code allows you to choose whether pressing Return results in just an LF, or a CR plus
an LF. Although the code looks the same in the code editor, the hidden characters representing the
line endings are different.
The Airbnb ESLint configuration is more picky than Visual Studio Code: It requires LFs only, to
keep code consistent across operating systems and code editors.
So how do you fix this error if you see it? Take a look at Visual Studio Code’s status bar, at the
bottom of the window (Figure 6.3, “Visual Studio Code’s status bar”). It indicates, using LF or CRLF,
what Visual Studio Code currently uses for new lines.
If you see CRLF, click it. In the pop-up window, select LF (Figure 6.4, “Updating the current file’s
end-of-line sequence”).
This change affects only your current file. Change the setting globally by selecting Code →
Preferences → Settings in the menu bar. Search for “eol” (for “end of line”) and change the
setting to \n (which is the regex equivalent of LF) (Figure 6.5, “Updating the end-of-line settings”).
(Though this global change automatically applies the setting to new files, it does not edit existing
files. That is why you used the status bar to make the change in Header.js.)
Similarly, Visual Studio Code’s default setting for tab size (the number of spaces used for an indent
made with a tab character) might be clashing with your ESLint rules. If so, you will see an error like
this:
Visual Studio Code allows 2 or 4 spaces for an indent; ESLint requires 2. To change Visual Studio
Code’s setting, click Spaces: 4 in the status bar. Then, click Indent Using Spaces. Finally, click 2.
This will change the settings for Header.js.
To update the setting globally, open Settings again (Code → Preferences → Settings). Search for
“tab size” and change the setting appropriately (Figure 6.6, “Updating tab size settings”).
Finally, you might see an error related to the eol-last lint rule, which requires an empty line at the
end of each file:
You can add the new line by simply pressing Return after the last line in the file. Or – since this is an
auto-fixable error – you can use one of the auto-fix methods discussed in the previous chapter to fix
the error and add the new line.
Save Header.js. Now import and add your new header component to App.js. While you are there,
delete some of the generic code that Create React App generated – you will not need it.
function App() {
return (
<div className="App">
<div>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit
{' '}
<code>src/App.js</code>
{' '}
and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<Header />
</div>
...
Save your files, start your development server (if it is not already running), and check your browser.
You will see your new component displayed on the page and listed in the React DevTools
Components tab (Figure 6.7, “Viewing the header in the browser”).
Although the component renders to the screen, it does not look like a header. It needs some styling.
In Ottergram, all the styles were in a single file, App.css. That worked because Ottergram was small
enough that you did not need to worry about organizing your styles.
But in larger, more complex apps, you will often see stylesheets for individual components.
Breaking styles up by component helps keep your code base organized and maintainable. That is
what you will do here.
In the components folder, create a file called Header.css. Add styles for the header’s image and text,
using header-component as the class name for the selectors.
.header-component {
display: flex;
margin-bottom: 10px;
box-shadow: 0px 9px 13px rgb(0 0 0 / 5%);
align-items: center;
padding: 0 20px;
}
.header-component img {
height: 50px;
width: 50px;
}
.header-component h1 {
font-size: 36px;
font-weight: 700;
color: #674836;
margin-left: 15px;
}
Now add the header-component class name to your component in Header.js. Remember to import
the stylesheet at the top of the file.
function Header() {
return (
<header>
<header className="header-component">
<img src={CoffeeLogo} alt="coffee logo" />
...
Save your files and reload Code Café in the browser. The header looks a lot better, and you have
now finished the first component of your new application (Figure 6.8, “Viewing the styled header in
the browser”).
At the beginning of this chapter, you added a folder called src/items. This
folder has the image files for all the available items, as well as a helper file
called index.js.
Open index.js and take a look at what it does. After it imports the image
files, it exports two variables: itemImages and items.
The second variable, items, is an array of the items available for sale. It
represents the information that you would normally store in a database,
including details about each item such as the title, price, imageId, and
itemId. Eventually, you will replace this variable with a call to an API. But
for now, you will use this mock data so you can continue to focus on your
React skills.
Create the component file and corresponding CSS file in the components
directory. Name the files Thumbnail.js and Thumbnail.css.
.thumbnail-component div {
background-color: #B9A28D;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
padding: 20px 40px;
}
.thumbnail-component img {
height: 106px;
width: 106px;
}
.thumbnail-component p {
display: flex;
width: 100%;
justify-content: center;
align-items: center;
margin: 9px 0 0;
font-size: 21px;
text-align: center;
white-space: nowrap;
}
import './Thumbnail.css';
There is an extra <div> tag wrapping the item image; it is there to add some
styling. Finally, the title and image variables are passed to the component
as props.
Prop Types
The Thumbnail component has two props: an image and a title. And ESLint
has a rule that requires all props to have a declared prop type.
ESLint will attempt to validate the prop types of each component in your app.
You have not added any prop types yet, so it will alert you that they are all
missing: As you have seen before, ESLint underlines problem areas with a
squiggly red line. In Thumbnail.js, it underlines the component props –
image and title.
Hover over each prop, and you will see the same error telling you that the
prop is missing in props validation (Figure 6.9, “ESLint prop types error”).
The first step to using prop types (and fixing these errors) is to install the
prop-types library.
prop-types is a library built and maintained by the React team. With it, you
can define the type of each prop in a component. It also serves as a runtime
check to ensure compatibility between the prop types defined for a
component and the props passed to the component.
You will need to run a new command in your terminal to install the library.
Since you already have Code Café running, you have two options: open a
new window or tab, or stop and restart your server. Either way is fine; just
make sure you have the server running again when you are done.
Thumbnail.propTypes = {
image: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
It might be surprising that you declare the image prop as the string type.
This is because its value points to an imported image. When an image is
imported as a variable, the result is either a string path pointing to the image
location or an inline Base64-encoded string, depending on the webpack
configuration. Details on how to edit the configuration are at the end of this
chapter.
You add .isRequired at the end of the type definition to specify that the
props are required by the component. When a prop is marked as required,
an error will warn you if the parent component does not provide the prop.
Now future developers will know that Thumbnail expects an image and a
title, both in string format.
Rendering Items
Now that you have built the Thumbnail component and identified the needed
prop types, it is time to use it to render the items in the browser.
Switch to App.js. You will need both the items and itemImages variables
that items/index.js exports, so import those at the top of the file.
Each item will need a key. The itemId for each item is unique, so you can
use it as the key.
import './App.css';
import Header from './components/Header';
import Thumbnail from './components/Thumbnail';
import { items, itemImages } from './items';
function App() {
return (
<div>
<Header />
{items.map((item) => (
<Thumbnail
key={item.itemId}
image={itemImages[item.imageId]}
title={item.title}
/>
))}
</div>
...
Save your files and check the browser to see all the items being displayed
(Figure 6.10, “Items displayed in the browser”).
Now customers can see all the items Code Café offers, which is great. But the
site does not look very stylish. Currently, the Thumbnails are displayed in a
column that is much too wide, and you have to scroll down to see them all.
You will fix that shortly.
Home Component
Before you take care of styling your home page, take a step back and look at
how you are getting the items onscreen: In App.js, you map over the items
array to create an array of Thumbnails.
Normally, App.js is responsible only for top-level data and routing to other
components. It is better to encapsulate the details of how a component is
built, such as how a Thumbnail gets its image and title, in another file.
In this case, you will create a new component called Home to render the
Thumbnails. You will display this component when the user navigates to your
home page or to /. It will also let you add the styling you need for your home
page.
In Visual Studio Code, create the files for the Home component and its
associated CSS stylesheet. Add the styles to Home.css first:
.home-component {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
grid-gap: 32px 10px;
padding: 32px 20px;
}
Now, in Home.js, import the stylesheet. Then create the Home functional
component, which should return a <div> with the home-component class
name. Export your newly created component with export default Home;.
Next, you need access to the items array. In the new component, accept the
items array as a prop. This will emulate fetching data at the top level of your
application. (When you do this, ESLint will flag an error related to prop types
in this component. You will address this error in a moment. Also, if you are
wondering why you do not import items, we will explain that at the end of
this chapter.)
{items.map((item) => (
<Thumbnail
key={item.itemId}
image={itemImages[item.imageId]}
title={item.title}
/ >
))}
Paste this code inside the <div> in the Home component. (ESLint will flag
another error related to prop types. You will fix this in a moment as well.)
Finally, import the itemImages object directly into the component. This is a
static object and not likely to change.
Now that Home is taking care of rendering the Thumbnails, add the new
component to App.js and delete the code you no longer need:
import './App.css';
import Header from './components/Header';
import Thumbnail from './components/Thumbnail';
import Home from './components/Home';
import { items, itemImages } from './items';
import { items } from './items';
function App() {
return (
<div>
<Header />
{items.map((item) => (
<Thumbnail
key={item.itemId}
image={itemImages[item.imageId]}
title={item.title}
/>
))}
<Home items={items} />
</div>
...
So far, you have looked at ESLint errors in your terminal and in Visual
Studio Code. ESLint errors also appear in your browser when your app runs
in development mode (but not in production). To see what that looks like,
open your browser with your app running. An overlay on the screen displays
several errors (Figure 6.11, “ESLint compilation errors”).
The Home component has only one prop – items – but two errors. ESLint
marks both the prop itself and the call to items.map with problem squiggles.
What is going on?
You intended for the items prop to be an array of objects. But ESLint does
not know that, because you have not defined the prop type. So when the Home
component calls items.map, ESLint has no way to verify that items has a
method called map.
There are two prop types that identify a prop as an array: array and arrayOf.
Use PropTypes.array to define a prop as an array of any type; use
PropTypes.arrayOf to specify the type of the items inside the array.
To make your code as well documented as possible, use arrayOf to define the
shape, or the underlying structure, of the objects contained in the array.
Home.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
itemId: PropTypes.string.isRequired,
imageId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
description: PropTypes.string,
salePrice: PropTypes.number,
}),
).isRequired,
};
Each item contains an itemId, imageId, title, and price. The price is a
number, and all the other fields are strings. All the keys are required.
Create a new src/types directory. Copy the code for the item prop type from
Home and add it to a new file in the types folder called item.js.
You import PropTypes here, just as you did in the component file. You also
export ItemType so you can use it in your components.
Save your file. Now update the copied code in Home to use your new variable.
...
import './Home.css';
import ItemType from '../types/item';
Home.propTypes = {
items: PropTypes.arrayOf(
PropTypes.shape({
itemId: PropTypes.string.isRequired,
imageId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
description: PropTypes.string,
salePrice: PropTypes.number,
}),
).isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
};
Now you can use ItemType throughout your app, without having to type
multiple lines of code and without having to keep the various prop types it
contains in sync.
Your code is clean and well organized. Save your files and take a look at
Code Café in the browser. Now it is just as well organized as your code
(Figure 6.12, “Completed components”).
For example, you just declared that Home should receive an array of objects
that each have specific fields, including price. What happens if the price of
an item is missing? To find out, open items/index.js and comment out the
price of an item, such as the cupcake. Then save the file.
ESLint does not immediately complain, and your website will load. Props
validation is a runtime check, so your app still compiles (provided there are
no other errors as a result of the mismatch). However, if you check the
DevTools console, you will see an error warning you of the missing prop:
Next, uncomment the price to restore it, but add single quotes around the
value, like price: '1'. Save the file again. Now the console shows a
different warning:
Console warnings like these can help minimize the time you spend
debugging because you forgot to pass a required prop or passed a value of
the wrong type.
Before you move on, undo the changes you made to items/index.js.
Conclusion
Your practice building components is paying off: Code Café is looking
great.
By defining needed prop types for your components, you make them more
robust, increase their reusability, and help prevent errors. You can quickly
see what data you need to use a component correctly (and can get instant
feedback if you are not using it correctly), saving you time and energy. This
is especially true when working on applications with many components and
many developers.
For the More Curious: Why Pass Items as a Prop?
Why did you pass the items array as a prop to the Home component instead
of importing it, like you did with itemImages?
Later, you will load the items from an API. Getting data from an API
should happen in only one place, not in every component that uses the data.
So passing items as a prop will make the later change more
straightforward. The itemImages, on the other hand, will continue to live in
your application, so it is safe to import them everywhere.
However, if you did not plan to replace items with data from an API, your
code would be cleaner if you imported items in Home rather than having an
extra prop.
So then why not pass the item object as a prop to Thumbnail (like item=
{item})?
By passing only title, you give the Thumbnail component only the details
it needs to render a given item as a thumbnail. (Note that key is a reserved
prop that React requires when rendering a list of items with map, so even if
you were to pass the whole item object to Thumbnail, you would still need
to specify key separately.)
For the More Curious: Inlining Images
Every server request takes a small amount of extra time beyond the time
needed to actually download the data. That extra time is called latency.
Inlining images helps avoid extra requests to the server, and therefore
latency, by encoding images as data URIs in Base64 and then inlining them
in the JavaScript or CSS file.
However, increasing the size of JavaScript files makes your application load
more slowly. Similarly, increasing the size of CSS files makes your styles
load and display more slowly. So there is a trade-off between inlining
images to reduce the number of server requests and decreasing the speed at
which styles and the whole application load.
By default, Create React App places images of less than 10,000 bytes
inline. If you want to change the limit for inlining images, you can do that
by setting an environment variable with the name
IMAGE_INLINE_SIZE_LIMIT. You can also disable inlining entirely by
setting the limit to 0. (You can set the environment variable as part of your
build or start command, or add it to the appropriate script in package.json.)
Only AVIF, BMP, GIF, JPG, and PNG images are inlined; SVG images are
not. To import an SVG as a component, Create React App provides special
syntax that renders the SVG inline and includes the SVG in your JavaScript
files:
You can then use the image as a component: <Logo />. Note that this means
you will need to change how you apply styles to your SVGs.
You have already seen that React makes it easy to add styles by importing
CSS and using the className keyword in components. In this chapter, you
will add more style rules to enhance the design of your application.
By the end of the chapter, Code Café will have a UI that looks good on both
mobile devices and desktop browsers and that has some polished features
(Figure 7.1, “Hover effect”). This will set the stage for the order functionality
you will begin adding in the next chapter.
But first, you will take care of some housekeeping and learn about some core
style concepts.
Create React App provides a default stylesheet: App.css. You are using your
own styles, not the default styles, so you no longer need this file.
Delete App.css, either by using your file explorer or by right-clicking the file
in Visual Studio Code and selecting Delete. Then remove the import for
App.css from the top of App.js.
import './App.css';
import Header from './components/Header';
...
function App() {
...
Style Scoping
Now for some core style concepts. Recall from Chapter 2, Components that
all CSS has a global scope, meaning that style rules apply throughout your
app. This is true even when style rules are split into multiple CSS files.
To see this in action, add a style rule in Header.css that specifies the color
for text in <p> tags.
...
.header-component h1 {
...
}
p {
color: #674836;
}
With your development server running, look at your browser. The titles for
the thumbnails have changed color (Figure 7.2, “Brown thumbnail titles”).
The thumbnail titles are inside <p> tags in the Thumbnail component, not in
the Header component. But the React build process merges all the CSS
together. So setting a style rule for the <p> tag affects <p> tags throughout
your app.
This is why you have been prepending all your CSS class names with
component names, such as .thumbnail-component and .home-component.
This naming convention keeps the class names unique and ensures that your
styles apply only where you want them to.
Add specificity to the style rule in Header.css by using the component name.
...
.header-component h1 {
...
}
p {
.header-component p {
color: #674836;
}
Check your browser again. The thumbnail titles are back to the correct color
(Figure 7.3, “Black thumbnail titles”).
Ultimately, Code Café should function and look good on both desktops and
mobile devices. Figure 7.4, “Desktop design” and Figure 7.5, “Mobile
design” show the target designs:
Mobile-first design is popular with sites that will be used mostly on mobile
devices. Desktop-first design is still useful when your site will be used
primarily on desktops, either because of the demographics of your target
audience or because it needs to display a lot of data on the page at once.
Should you grab a mobile device and connect it to verify that your styles
work for smaller screens? No need. The DevTools can show you what your
site will look like on various screen sizes and under various conditions.
Try it out: Open the DevTools. In the top menu bar, the second icon toggles
the device toolbar (Figure 7.6, “Toggling the device toolbar”).
Click the icon to toggle the device toolbar and preview how your website will
look on a different device. In the toolbar, click the first dropdown (which has
either the name of a mobile device or Dimensions: Responsive) and select
iPhone 12 Pro to see how it looks (Figure 7.7, “Previewing the site on
iPhone 12 Pro”).
In Chapter 6, Prop Types, you styled the Home component with grid styling.
This means that Home uses the CSS Grid Layout element, which creates
responsive rows and columns that take up the available space.
Now switch back to the desktop view by toggling the device toolbar off.
Compare what you see with the target design above.
The thumbnails are a little small. You need to make them larger – but only on
desktops, not on mobile devices.
CSS media queries let you create styles that respond to the screen size and
other conditions, so that your site looks just right on any screen.
Media Queries
Open Home.css.
Based on this rule, a column’s minimum width is 150px, and the maximum
width is 1fr – or one “fraction” of the available space. In this case, that means
the columns will expand evenly to take up any extra space.
Add a media query so the columns are slightly larger on larger devices, using
768px as the screen-width breakpoint.
...
.home-component .thumbnail-component:nth-child(3n + 3) div {
background-color: #3F3F40;
}
A media query tells React, “When the conditions the app is running in match my
specified conditions, use the styles defined here instead of the styles defined
elsewhere.”
Here, the media query says that when the device width is at least 768px, the
minimum width of the grid columns should be 200px. It also specifies larger
values for the padding (40px on the top and bottom of the component and 50px
on the left and right) and grid-gap (40px between the rows and 15px between
the columns).
Save the file and check out your browser (Figure 7.8, “Thumbnails in the
browser”).
Nice! Now that the thumbnails are a little larger and have a little more elbow
room, they look good on the browser screen. Switch to the device view in the
DevTools to confirm that the site looks the same as before on a mobile device.
The new CSS rules you added apply only when the screen is at least 768px
wide.
Now switch back to the desktop view. Although the thumbnails look good, the
header could use some attention. The padding you added has shifted the
thumbnails over, leaving the header out of alignment. And now that the
thumbnails are larger, the header image looks a little small.
Add a media query to Header.css using the same screen-width breakpoint,
768px. Use it to pad the header and increase the size of the image.
...
.header-component p {
color: #674836;
}
.header-component img {
height: 60px;
width: 60px;
}
}
Much better – now everything is aligned again (Figure 7.9, “Header in the
browser”).
To accomplish this, you will use the transform property, which can alter the
shape, size, rotation, and location of an HTML element without interrupting
the flow of the elements around it.
Open Thumbnail.css. Add a CSS rule to make the image scale up when the
user hovers over the thumbnail.
...
.thumbnail-component p {
...
}
.thumbnail-component:hover img {
transform: scale(1.2);
}
The target element for the transformation is the <img> tag – you want the
image to get larger, not the entire thumbnail. But you apply the pseudo-class
:hover to .thumbnail-component, rather than to the <img> tag, so that the
image scales if the mouse is anywhere over the thumbnail, including the text.
transform: scale(1.2) tells the browser to draw the target element at 120%
of its original size.
Save the file and test the change in your browser. The images grow larger
when you hover over the thumbnail. (The effect is also visible on mobile
devices when the user presses and holds a thumbnail.)
Let’s use the transition property to animate the change. CSS transitions
create a gradual change from one visual state to another, which is just what
you need to make the transformation more polished.
...
.thumbnail-component img {
height: 106px;
width: 106px;
transition: transform 333ms ease;
}
...
The transition style rule you added to the <img> tag has three parts.
First is the property you are targeting. Here, you tell the browser that it will
need to animate changes to the transform property.
The second part is the duration of the transition. You specify that this
transition should take 333 milliseconds. Feel free to play around with this
value to see the effects of longer and shorter durations.
The last value you supply is the timing function. By default, the browser uses
the ease timing function, which makes the transition faster in the middle than
at the start or end.
Save the file and hover over some thumbnails in the browser to see the
transformation happen again. Now the transformation happens gradually and
provides just the pop you need (Figure 7.10, “Completed hover effect”).
Figure 7.10. Completed hover effect
You can also use transitions for other properties, such as background-color
or margin. Targeting all animates all style rule changes. You can learn more
about transitions, including the other options for timing functions, at
developer.mozilla.org/en-
US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions.
Conclusion
Now your components showcase the items for sale at Code Café – and
because of the styles you added, they look great.
For the rest of this book, we will provide most of the styles you need so that
you can focus on honing your React skills. But feel free to experiment with
different style rules along the way. React makes it easy to add styles using
the className attribute.
Now that you have built your app foundation, it is time to add functionality
by fetching data from an API. You will do that in the next chapter.
For the More Curious: Grid Layout
If you are not familiar with CSS Grid Layout, this game is a good
introduction to it: cssgridgarden.com.
For the More Curious: Styled Components
Another way to add styles to your components is to use styled components.
This approach uses an external library to let you write CSS in your
JavaScript files. The styles are then automatically scoped to the component.
You can read more about styled components at styled-components.com.
For the More Curious: Compiled CSS
Instead of styled components, you can also use a CSS preprocessor such as
SCSS, Sass, Less, or Stylus to make scoping the CSS to a single component
easier. These options also provide other benefits, such as variables, loops,
and functions to generate CSS.
To make this happen, you will first set up a back-end server and then make a
network request to load the items from the server.
cd YOUR_PATH/resources/code-cafe-backend
npm install
npm start
In this chapter, you will focus on retrieving the list of items from the API at
the endpoint /api/items.
(We will tell you what endpoints to use as you need them. However, if you
would like to know all the endpoints on the server, you can see a list in the
section called “For the More Curious: Server Endpoints” near the end of this
chapter or in the resources/code-cafe-backend/README.md file.)
The list of items that the server returns will take the place of the items array
that you currently import into App.js.
Like the React server, the back-end server will run until you stop it with
Control-C. You will need the server running whenever you work on Code
Café, so remember to start it each time you work on the project. If you do
not, you will get network errors. (And if you do see network errors, check
that the server is running and see whether the console has logged any errors.)
Now that you have the server running, you need to set up Code Café to begin
interacting with it.
Creating a proxy
Although the servers for Code Café and the back end both run locally, they
run on different ports. By default, to protect against cross-site script attacks,
the browser blocks API requests to a different origin, including a different
port. You need to let your development server know that it is safe to
communicate with the back-end server.
Instead of dealing with the complexities of CORS policies, you will set up
Code Café to act as a proxy to the back-end server. This way, you will not
have to make any allowances for cross-origin sharing.
This is a common pattern, so Create React App makes it easy to set this up in
the development servers it creates. Open package.json in Code Café and add
a proxy.
...
"scripts": {
...
},
"proxy": "http://localhost:3030",
"eslintConfig": {
...
Save package.json. If you are currently running the Code Café development
server, stop the process in the terminal with Control-C, and start it again with
npm start. (Here is a tip: Instead of retyping the start command, you can
duplicate the most recently run command in the terminal by pressing the up-
arrow key. Press Return to run it.)
There is one more step to setting up Code Café’s back-end server: You need a
way to make HTTP requests.
There are many tools you can use to make HTTP requests in React
applications, including the browser’s built-in Fetch API. React is not
opinionated about this. We like the popular Axios library, because it is a
lightweight package that provides a simple syntax for making network calls,
working with JSON data, and handling errors.
Go ahead and install Axios. In a terminal separate from the two servers you
have running, navigate to your code-cafe directory. Then install Axios by
running
When a user visits Code Café, the app should retrieve the list of items from
the server and then load them on the screen. This request should happen
only the first time the component renders.
Each time the component renders, React compares the current values of the
variables in the array with the values from the previous render. If any value
has changed, React executes the function. Otherwise, React skips the
function until the next render cycle.
Use the useEffect hook in App.js to make an HTTP request to get the list
of items.
return (
...
The first argument you supply to useEffect is the callback function. This
function uses the Axios library to make a GET request to the API.
The second argument is the dependency array, which should contain all the
dependencies of the effect. During each subsequent render, React compares
the contents of the dependency array with the previous values. React uses
shallow comparisons (that is, reference equality) to determine whether the
contents of the array have changed. React executes the callback only if it
detects changes within the dependency array.
Here, you use an empty dependency array. There are no values within the
array that can change from render to render. This ensures that React will run
the callback function only once, after the initial render.
Normally, to keep the code in sync, you must include in the dependency
array the values (including functions) that you define within your
component and use inside useEffect. Because axios is defined outside
your component, it is not a part of the dependency array.
(The setter function returned from useState is a special case, which you
will see in a moment.)
You should see a request made to items as the page loads (Figure 8.6, “items
requests”).
"proxy": "http://localhost:3030"
with
"proxy": "http://127.0.0.1:3030"
Then restart your React application and try again.)
You might recall from Chapter 1, Create React App that Create React App
automatically renders your app using React.StrictMode, a developer tool
that checks your code for potential problems and provides warnings when
issues are detected.
One way this tool tests your components is by mounting a component and
then simulating its destruction and re-creation immediately afterward.
Because of this, when a component mounts, you will often see code run
twice, including side effects like API calls. This happens only in development
mode, because React.StrictMode does not run in production.
Click one of the requests to see more information about it. The preview pane
within the Network tab shows a formatted version of the response from the
server (Figure 8.7, “Preview pane”).
Use the useState hook to add a new state value called items to App. Set the
initial value to an empty array. When the request is successful, use the setter
function to update the state value with the data retrieved from the server.
(This code includes some new syntax, such as then and catch. We will
explain it in a moment.)
function App() {
const [items, setItems] = useState([]);
useEffect(() => {
axios.get('/api/items');
axios.get('/api/items')
.then((result) => setItems(result.data))
.catch(console.error);
}, []);
...
Now useEffect has a dependency that you use within the function:
setItems. Do you need to add setItems to the dependency array? No,
because React guarantees that the reference for setter functions will not
change throughout renders. This is an exception to the normal pattern of
including in the dependency array the values that you define within your
component and use inside useEffect.
The data from the API is slightly different from the data that was in the local
items.js file: now “Drip Coffee” is called “Coffee”, and “Chocolate Milk”
is just “Milk.” Check out your browser to make sure you are seeing the new
data (Figure 8.8, “Viewing items from the server in the browser”).
Now, what are then and catch? They are JavaScript constructs for dealing
with asynchronous events, such as getting data from the server.
The library you are using to make your network requests, Axios,
automatically returns a promise when you make a request. To specify the
action that occurs after the promise is fulfilled, you use the promise
construct then. Code inside the then block does not execute until the
promise is complete. If there is an error, the catch block executes.
For many developers, promises are one of the most confusing aspects of
JavaScript.
'use strict';
const getData = () => {
console.log("getData Running");
return Promise.resolve("yay!");
};
console.log("top");
getData().then((result) => {
console.log("result", result);
}).catch((error) => {
console.error("error", error);
});
console.log("end");
Read through the code. There are seven strings that can print to the console.
What do you think will be printed, and in what order?
Ready to find out the answer? This demo code is also in your downloaded
resources file, in code-cafe-resources/demos/promiseDemo.js. You can
run it using node on your machine. Open a new terminal window, navigate
to the demo, and run it:
cd YOUR_PATH/resources/code-cafe-resources/demos
node promiseDemo.js
The first thing that logs is top. Although the getData function is defined
above console.log("top"), the code invokes the log statement first.
Next, the code invokes getData, which causes getData Running to log.
Then, when it returns Promise.resolve("yay!"), it starts a promise.
JavaScript does not wait for the call to then to start the promise – the
promise starts immediately when you invoke it.
Feel free to open promiseDemo.js in your editor, make changes, and run the
code again with node promiseDemo.js to see the result.
You will have opportunities to practice working with promises and other
asynchronous API requests in upcoming chapters. We will also discuss
interacting with promises using async/await and try/catch blocks.
Conclusion
Nice work! You have integrated Code Café with the API and made a
successful network request to display the available items. In the next
chapter, you will add routing to your app to make it more interactive.
For the More Curious: Server Endpoints
Table 8.1, “Code Café server endpoints” summarizes the Code Café server
endpoints that the app can interact with:
You will use many of these endpoints as you progress through the book.
For the More Curious: How to Set Up a Proxy in
Production
The proxy you set up in package.json affects only local development,
when you run npm start. It will not do anything when you run a
production build on the server. In production, you need to make sure that
the web server – which serves the static files that React builds – is also
running a proxy. If your server is running Node.js, one popular package to
use for setting up a proxy is http-proxy-middleware. See
www.npmjs.com/package/http-proxy-middleware.
For the More Curious: Cross-Origin Resource
Sharing (CORS)
CORS occurs when a resource is requested from a domain that does not
match the current domain. Although the browser allows scripts and
stylesheets to be loaded from other origins, it blocks xhr requests,
XMLHttpRequests, and fetch requests made to other origins. (Axios makes
XMLHttpRequests internally.)
Although you can use useEffect to perform tasks that those lifecycle
events previously triggered – such as fetching data when the component
initially renders – there is not a one-to-one substitution. useEffect captures
the values of the component props and state during each render. This means
that it is easy to misuse the useEffect hook, resulting in unexpected
behavior.
What do you think will happen when you run the code?
Run it and see the result. Make some changes to the code to learn more
about promises, and run it again to see what happens.
Chapter 9. Router
The home page looks great. Visitors to Code Café find a stylish app and can
easily see what items are available for purchase.
In this chapter, you will take a step toward fixing that. It is time to introduce
routing to your application.
In React, you use routing to keep the UI in sync as a user navigates through
the application. Remember that React is declarative, so you define the UI for
a URL, and React will render the elements needed to make it happen.
You will use the popular React Router library to create routes in your
application. By the end of this chapter, clicking a Thumbnail item will route
the user to a new URL and display the item’s name (Figure 9.1, “Item details
page”). Later, you will add the functionality for customers to see details about
the item and, finally, order an item from the details page.
React Router is the most popular router to use in combination with Create
React App. Other React frameworks, such as Next.js, ship with their own
routing libraries.
function App() {
...
return (
<div>
<Router>
<Header />
<Home items={items} />
</div>
</Router>
);
...
The Router component in App.js wraps all the content in the application, so
all components are its descendants.
The React Router library exposes a few different routers you can select from.
By design, all the routers have the same interface, so they are
interchangeable. The difference is how navigation affects the URL:
BrowserRouter uses the browser’s built-in History API, so URLs look
like /details/apple.
Creating Routes
Your browser URL is currently localhost:3000/. Begin implementing
routing in your app by adding a route that renders the Home component at the
index path, using /.
...
import {
BrowserRouter as Router,
Routes,
Route,
} from 'react-router-dom';
...
function App() {
...
return (
<Router>
<Header />
<Home items={items} />
<Routes>
<Route path="/" element={<Home items={items} />} />
</Routes>
</Router>
);
...
You import two components to set up the route – the Routes and Route
components. Each has a unique job. Let’s examine the components that App
returns, from the inside out.
You are still passing items to the Home component. The element prop accepts
valid JSX, so passing props to a component inside a Route works the same
way as passing props to any other component.
The Routes component contains one or more Route components. Most apps
have a single Routes component, and Code Café will have only one.
However, a more complex app might use multiple Routes to handle
navigation in different sections of the site. The Routes component’s job is to
pick which content to display. It analyzes the paths in all its child Route
components and picks the one that best matches the URL. The correct
element then renders from the matching Route.
Previous versions of React Router selected the first matching Route. In more
recent versions, Routes analyzes all the options before selecting the best
match, so you do not have to worry about putting the Route components in a
particular order inside the parent Routes.
Recall that routing works only within the context of a Router. Though all
Routes components must be descendants of the Router component, they do
not need to be direct children. The Router and Routes do not even have to be
in the same component. However, it is common to see them both in App.js.
Save App.js and make sure your browser points to http://localhost:3000/,
with nothing after the /. There are no visible changes at this point – you
should see the same page content as before.
Now you know what happens when you visit /. Next, try this: Open
http://localhost:3000/unknown in your browser.
All you get is a blank page. React Router uses exact matching, and you do
not have a Route that lists /unknown as a path. When React Router finds no
matching Route, it renders nothing.
In the path that you pass to a Route, you can use the * operator as a wildcard
to match any string. Add a new Route in App.js to catch invalid paths.
...
function App() {
...
<Routes>
<Route path="/" element={<Home items={items} />} />
<Route path="*" element={<div>Page Not Found</div>} />
</Routes>
...
The new route matches any URL and acts as a catchall. If the Routes
component cannot find a better match, the “Page Not Found” <div> element
will render.
In your downloaded resources file, copy all the files from the stylesheets
directory into your project’s src/components directory, so that they will be
ready to import when you need them. Be sure to replace Header.css with the
updated version.
Using Link
Now users can see that the URL they tried to visit is invalid. Next, it would
be helpful to give them a way to navigate to the home screen.
That would be more work than the Route component should do on its own.
To keep your code organized, build a new component to render for invalid
paths. Then use it to replace the <div> that you currently pass to the Route
component.
Make sure that you have copied the provided stylesheets into your project, so
that NotFound.css is in your app’s components folder. Then create a new file
(also in your components folder) called NotFound.js, and build your new
component.
function NotFound() {
return (
<div className="not-found-component">
<h2>Page Not Found</h2>
<Link to="/">Return Home</Link>
</div>
);
}
You import the Link component in NotFound.js from the React Router
library. It renders as an HTML <a> tag in the browser. But because you used
BrowserRouter, React Router adds click event handlers behind the scenes.
This means that the Link changes the user’s location using the HTML5
History API, rather than full-page refreshes.
Now add your new component to the route for the /* path.
Example 9.5. Adding the NotFound component to the route (App.js)
...
import Header from './components/Header';
import Home from './components/Home';
import NotFound from './components/NotFound';
function App() {
...
<Routes>
<Route path="/" element={<Home items={items} />} />
<Route path="*" element={<div>Page Not Found</div>} />
<Route path="*" element={<NotFound />} />
</Routes>
...
Save all your files and visit http://localhost:3000/unknown again to see the
new 404 page (Figure 9.3, “New “Page Not Found””).
But first, you need a details page for the route to display. The details page will
have two sections:
Begin with the sidebar. You already have the list of available items that you will
display there.
Open Home.js and look at how the thumbnails are displayed: The Home
component uses the array map method to iterate through the items and return a list
of Thumbnails to display. You will use the same method to create the sidebar.
Make sure that you have copied Details.css from your downloaded resources
file into your app’s components folder. Then, in Visual Studio Code, create a new
components/Details.js file and build your new component.
To build the sidebar, copy the JSX from the Home component and paste it into the
new file. Update the className to access the sidebar styles included in
Details.css.
For now, add a placeholder for the item detail portion of the screen. You will
come back to that soon.
Details.propTypes = {
items: PropTypes.arrayOf(ItemType).isRequired,
};
The placeholder text {/* display item */} might look a little strange. This is a
JavaScript block comment nested inside JSX. When adding comments inline, you
must wrap them in curly braces to ensure they do not render as part of the JSX.
...
import {
...
} from 'react-router-dom';
import Details from './components/Details';
import Header from './components/Header';
...
function App() {
...
<Routes>
<Route path="/details" element={<Details items={items} />} />
<Route path="/" element={<Home items={items} />} />
<Route path="*" element={<NotFound />} />
</Routes>
...
Nesting a route
How can you use routing to insert information about an individual item into the
Details component? You can nest a route inside the /details route. Do this in
App.js:
...
function App() {
...
<Routes>
<Route path="/details" element={<Details items={items} />} />
<Route path="/details" element={<Details items={items} />}>
<Route path=":id" element={<div>Detail Item</div>} />
</Route>
<Route path="/" element={<Home items={items} />} />
<Route path="*" element={<NotFound />} />
</Routes>
...
For now, your detail information is just the text “Detail Item.”
The full path of the child route is /details/:id. This path looks a little different
from the other ones you have seen so far. :id is a dynamic parameter, which we
will discuss later in this chapter. For now, what you need to know is that you can
use :id to reference the itemId of one of the items. (For a list of all the itemIds
available, check out src/items/index.js.)
Unfortunately, only the sidebar shows up – the “Detail Item” text does not. This
is because the new element for the route is not rendering. Fix this by adding a
new Outlet component from the React Router library to Details.js.
You replace the placeholder in the Details component with the React Router
component Outlet. When there are nested routes, React Router analyzes each
child Route to find the matching path. The parent component then uses Outlet to
render the matching child Route element.
Make sure you have copied DetailItem.css from your downloaded resources
file into your app’s components directory.
import './DetailItem.css';
function DetailItem() {
return (
<div className="detail-item-component">
Detail Item
</div>
);
}
...
import {
...
} from 'react-router-dom';
import DetailItem from './components/DetailItem';
import Details from './components/Details';
...
function App() {
...
<Routes>
<Route path="/details" element={<Details items={items} />}>
<Route path=":id" element={<div>Detail Item</div>} />
<Route path=":id" element={<DetailItem />} />
</Route>
...
Index route
There is one more nested route to add under /details. It will be an index route.
When the Outlet component is on the parent path (in this case, /details, with
no item ID appended), it renders an index route.
Add a nested index route that displays the text “No Item Selected.”
...
function App() {
...
<Route path="/details" element={<Details items={items} />}>
<Route path=":id" element={<DetailItem />} />
<Route index element={<div>No Item Selected</div>} />
</Route>
...
Here, you use the index keyword, because this index route is a child of another
route. But earlier, when you created the home route – which renders the Home
component at the app’s index path – you did not use this keyword. Instead, you
specified a path of /. This is because your home route is not a child of another
route.
Navigate back to the parent path, http://localhost:3000/details/. Now, instead of a
blank screen, it displays “No Item Selected” (Figure 9.7, “Displaying the “No
Item Selected” index route”).
the parameter name (id, in this case), which follows the colon
How do you get the value of the parameter in your code? React Router
provides a hook called useParams that returns an object containing all
parameters from the current path. (Recall that hooks are JavaScript functions
you can use to access state within functional components.)
function DetailItem() {
const { id } = useParams();
return (
<div className="detail-item-component">
Detail Item
{id}
</div>
);
}
export default DetailItem;
In the next chapter, you will update DetailItem to show the item’s icon and
price. For now, you have more navigation to work on.
Navigating Home from the Header
It would be nice if there were an easy way to get back home from the
details page – or any other page you might add to your site, for that matter.
On many websites, clicking the title or logo in the header sends you back to
the home page. Edit the header to add a link that navigates the user back to
the home page.
function Header() {
return (
<header className="header-component">
<img src={CoffeeLogo} alt="coffee logo" />
<h1>Code Café</h1>
<Link to="/">
<img src={CoffeeLogo} alt="coffee logo" />
<h1>Code Café</h1>
</Link>
</header>
...
You use React Router’s Link component to route the user back to the path
/. You wrap both the title and the image in the Link component, so the app
will redirect the user no matter where they click.
Save your file and test your new navigation in the browser before
continuing.
Navigating from Thumbnails to Details
Now let’s update the Thumbnail component. Clicking a Thumbnail should
direct the user to the item’s details page.
To do this, you will need to pass the itemId prop to the Thumbnail. This
means you will need to add the prop everywhere that you use the Thumbnail
component.
Visual Studio Code can help you find all the instances of something
throughout your application. Press Command-Shift-F (Ctrl-Shift-F) to access
the pane for searching across files. Then enter “<Thumbnail” to find all
instances of the Thumbnail component (Figure 9.9, “Search results”).
...
function Details({ items }) {
...
<Thumbnail
key={item.itemId}
image={itemImages[item.imageId]}
title={item.title}
itemId={item.itemId}
/>
...
...
function Home({ items }) {
...
<Thumbnail
key={item.itemId}
itemId={item.itemId}
image={itemImages[item.imageId]}
title={item.title}
/>
...
Finally, swap the existing anchor tag for the new Link component in
Thumbnail.js.
Thumbnail.propTypes = {
itemId: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
...
Because the path for each Thumbnail is based on the specific item, you use a
template literal, specified by backticks, to compute the correct path from the
itemId prop.
Are you wondering why you did not use a Link to begin with? Link comes
from the React Router library and must be a descendant of a Router
component. You did not have access to it until you added the library and set
up your Router.
Save your files, then check out your home page and click one of the items.
Code Café navigates to the details page for that item. Click the site’s title or
icon in the header to go back home, or click another item in the sidebar to see
its details page. All your navigation should be running smoothly.
Conclusion
Routing allows users to interact with your app and enhances the overall user
experience. Using React Router, React works to keep the UI in sync; new
information displays onscreen when a user navigates to a new URL.
Now each of your thumbnails is clickable and takes users to a new page
where they can learn more about an item. You also added links to help your
users navigate back to the home page and to keep them from getting stuck.
In the next chapter, you will learn about another useful feature of React,
conditional rendering, as you build out the item details page.
Silver Challenge: More Routes
Code Café is adding a loyalty program! Customers can learn about each tier
at /rewards/:tier – for example, /rewards/gold for the gold tier.
Add a /rewards/:tier route that displays the name of the tier from the
route parameter. For example, a user visiting /rewards/gold should see
“gold” as the content of the page.
Chapter 10. Conditional Rendering
In the last chapter, you implemented routing in Code Café using the React Router
library. Routing is useful because it gives you, the developer, control over which
elements will render based on the user’s location in your app.
Conditional rendering lets you specify which parts of a component should render
based on the state of your app. This keeps the UI and the application state in
sync. Because components are JavaScript functions, you implement conditional
rendering using JavaScript conditional syntax.
In this chapter, you will continue building the DetailItem component, using
conditional rendering to display all the information a user needs about the items
for sale (Figure 10.1, “Completed DetailItem”).
You will need access to the items array, so pass it as a prop to the DetailItem
component in App.js.
...
function App() {
...
<Route path="/details" element={<Details items={items} />}>
<Route path=":id" element={<DetailItem />} />
<Route path=":id" element={<DetailItem items={items} />} />
<Route index element={<div>No Item Selected</div>} />
</Route>
...
Now, with access to the items array in DetailItem, you can find the specified
item and display its title, image, and price. Add this item information to your
DetailItem component.
(During this step, you might run into an error in your browser. In the next step,
you will learn what is causing the error and how to fix it.)
function DetailItem() {
function DetailItem({ items }) {
const { id } = useParams();
const detailItem = items.find((item) => item.itemId === id);
return (
<div className="detail-item-component">
{id}
<img
className="details-image"
src={itemImages[detailItem.imageId]}
alt={detailItem.title}
/>
<h2>{detailItem.title}</h2>
<div>
$
{detailItem.price.toFixed(2)}
</div>
</div>
);
}
DetailItem.propTypes = {
items: PropTypes.arrayOf(ItemType).isRequired,
};
You use the JavaScript array method find to iterate through the list of items and
find one with an itemId that matches the ID from the parameters.
Using toFixed takes care of both problems by rounding and making sure the
result has exactly the specified number of decimal places. The return value is a
string because numbers cannot store an exact number of decimal places and are
subject to internal rounding errors.
Save your files. From Code Café’s home screen, click the coffee thumbnail to
navigate to http://localhost:3000/details/coffee (Figure 10.2, “Coffee details”).
If you have not encountered the error, trigger it now by manually navigating
to http://localhost:3000/details/coffee or by refreshing your browser.
Oops! The app crashes, and the browser displays a blank screen.
The browser console is often the best place to start investigating when you
run into a problem with your application. Open the Console tab in the
DevTools.
src={itemImages[detailItem.imageId]}
The error message indicates a problem with trying to read the value for
imageId, which is associated with the DetailItem variable. You created
DetailItem earlier in the component, using the array method find to find an
item whose itemId matches the URL parameter id. When there is no
matching item, the find method returns undefined.
DetailItem relies on the items array from the App component to find and
display the item onscreen. When the browser refreshes, App has to refetch
items from the server. Until the server request is complete, DetailItem will
be undefined.
So when you refresh the browser through manual navigation, DetailItem is
initially undefined. This is why DetailItem throws an error when it tries to
access detailItem.imageId. This error propagates up through Details to
App, which means the app crashes and the page is blank.
A page crashing is not a good user experience. You cannot control how long
it takes until DetailItem has a defined value. But instead of crashing, you
can show a loading message while the items request is completing.
if Statements
One way to implement conditional rendering in React is with a JavaScript if
statement. Add some conditional logic in App.js:
...
function App() {
...
useEffect(() => {
...
}, []);
if (items.length === 0) {
return <div>Loading...</div>;
}
return (
...
Save your file. From the details page, refresh your browser again. The app no
longer crashes while the items are loading. Instead, you briefly see the new
loading screen before the coffee details appear (Figure 10.4, ““Loading…”
rendered”).
Recall that you used useState to set the initial value of items to an empty
array, which has a length of 0. Therefore, items.length === 0 is initially
true, so JavaScript returns the JSX from inside the if block, and React
renders “Loading…” to the page.
Once the server responds with items and the contents of the array update in
useState, the page re-renders. Because items.length is no longer 0,
JavaScript skips the if block and continues to the return statement, which
contains the full application.
Inline Logical Operators
if blocks are a great way to tell React what to render when there are multiple
possibilities. But in a complex component, the extra return statements from
if blocks can make it difficult to tell at a glance what is rendering. A single
return statement that covers all the possibilities can help keep your code
cleaner and easier to read.
Recall that JSX – such as App’s return statement – can contain JavaScript, as
long as you wrap it in curly braces ({}). React uses the result from evaluating
the code inside curly braces. Because if blocks do not return values inline,
you cannot use them directly inside curly braces. However, JavaScript has
other inline operators you can use directly inside curly braces to conditionally
render elements.
Besides keeping your code cleaner and easier to maintain, inline operators
also allow you to reuse parent elements if needed. Refactor your loading
message in App.js to use a ternary inside the parent <div>, which will allow
the header to show while the items are loading. (You will need to indent the
<Routes> element and its contents.)
...
function App() {
...
useEffect(() => {
...
}, []);
if (items.length === 0) {
return <div>Loading...</div>;
}
return (
<Router>
<Header />
{items.length === 0
? <div>Loading...</div>
: (
<Routes>
...
</Routes>
)}
</Router>
);
}
A ternary has three parts, separated by the ? and : operators (Figure 10.5,
“Parts of a ternary”). The first part is the predicate, the condition that will be
evaluated, followed by ?. The second is the code that executes if the predicate
is truthy, followed by :. And the third is the code that executes if the
predicate is falsy.
Save your file and check out the app again in your browser. Because the
loading message is now nested inside the parent <div>, the app header
always displays (Figure 10.6, ““Loading…” rendered with the header”). And
as a bonus, your code is clean and easy to read.
Similar to before, the app crashes and shows a blank screen. The error in the
console is the same, pointing to DetailItem being undefined (Figure 10.7,
“Undefined item console error”).
There are at least two states your component can be in after loading is
complete:
the happy state, where DetailItem is a valid item object
Similar to what you did in App above, use a ternary in DetailItem to display
the item details or a message reading “Unknown Item,” depending on the
validity of DetailItem. This code will contain some new syntax, which we
will explain shortly. (Once again, you will need to indent the existing code
that you are embedding in the new ternary.)
...
function DetailItem({ items }) {
...
return (
<div className="detail-item-component">
{detailItem ? (
<>
<img
...
/>
<h2>{detailItem.title}</h2>
<div>
...
</div>
</>
) : <h2>Unknown Item</h2>}
</div>
);
}
DetailItem.propTypes = {
items: PropTypes.arrayOf(ItemType).isRequired,
};
Save your file and refresh your browser to see the new “Unknown Item”
message (Figure 10.8, “Unknown Item”).
One option is to wrap the elements with a <div>. Although this would work,
it would introduce unnecessary complexity to your app and could have
unintended effects on styling, among other things. The only reason to use a
<div> would be if you need it for applying styles.
Fragments behave differently from <div>s or other JSX elements. To see this,
navigate to the details page of a known item, such as /coffee for coffee.
Right-click the coffee image and choose Inspect Element to find the
element in the DevTools Elements tab (Figure 10.9, “Inspecting the
fragment”).
The <img>, <h2>, and price <div> are all direct children of <div
class="detail-item-component">. The empty element is nowhere to be
seen.
Fragments do not render in the browser, which means they do not create extra
nodes in the DOM. This is the difference between using a fragment to group
elements in JSX and using an element such as a <div>.
To begin, check out the list of items from the server in the DevTools Network tab
(Figure 10.10, “items in the Network tab”). (Click items in the list of requests to open
the preview pane.)
Expand items 0 and 1 (coffee and cookie) to see all of their fields:
{
itemId: 'coffee',
imageId: 'coffee',
title: 'Coffee',
price: 0.99,
description: '',
salePrice: 0,
},
{
itemId: 'cookie',
imageId: 'cookie',
title: 'Cookie',
price: 1,
description: 'May contain nuts.',
salePrice: 0.50,
}
These items have two fields that the other items do not: description and salePrice.
(You can expand the other items to check this.)
The description and salePrice information does not yet display in the DetailItem
component. Because only some of the items have these data fields, you will use
conditional rendering to display this information only when it is available from the
selected item.
Before rendering the item description, you will check whether the field exists on the
selected item using the logical AND.
...
function DetailItem({ items }) {
...
<h2>{detailItem.title}</h2>
{detailItem.description && <h6>{detailItem.description}</h6>}
<div>
...
JavaScript represents the logical AND operator with &&. This operator returns either the
first falsy value it encounters or, if all values are truthy, the last value of the statement.
Let’s see the results of this statement, starting with the cookie item.
The cookie object’s detailItem.description (the first value in the logical AND
statement) is a string. Non-empty strings are truthy, so the code returns the last item of
the statement, which is the JSX to render the description to the screen.
Save your file and look at your browser to see the new description field for the cookie
item (Figure 10.11, “Conditionally rendering the cookie description”).
Click one of the other item thumbnails, such as the croissant. Because it has no
description field, detailItem.description is undefined, which is falsy. React skips
the statement, and no description renders.
We said that React ignores the statement when the logical AND returns a falsy value
“in most cases.” The exceptions to this rule are the values 0 and NaN (or “not a
number”). When the logical AND operator returns either of these values, React does
not ignore the statement. If the variable you are evaluating might be one of these
values, you can convert it to a boolean using !! (double NOT) or Boolean to avoid this
problem.
The || operator
coffee and cookie also have fields called salePrice. When an item has a sale price
assigned, the sale price should override the regular price. Use the JavaScript logical OR
operator, represented by ||, to display the sale price or the regular price.
...
function DetailItem({ items }) {
...
<div>
$
{detailItem.price.toFixed(2)}
{(detailItem.salePrice || detailItem.price).toFixed(2)}
</div>
...
Save your file and navigate once again to the cookie item in your browser. Now
cookies are on sale for $0.50 instead of $1.00 (Figure 10.13, “Cookie sale price”).
Figure 10.13. Cookie sale price
As with the && operator, React renders the result from the JavaScript expression. If the
code preceding || evaluates to a truthy value, JavaScript returns that value, and React
ignores the rest of the statement. If the code evaluates to a falsy value, JavaScript
returns the code following ||, and React renders its value.
Because cookie.salePrice is a truthy value, that value renders. For items with no sale
price, the value of detailItem.salePrice is undefined, and React renders the second
value of the statement, the price.
What happens when the first value is 0, as it is for the coffee item? Click the coffee
thumbnail to find out (Figure 10.14, “Coffee is not on sale”).
Suppose you really want to offer coffee for free. You will need a different solution.
The ?? operator
The JavaScript nullish coalescing operator, represented by ??, works very similarly to
the OR operator. The difference is in how they handle falsy values: Unlike the OR
operator, the nullish coalescing operator returns any left-side value from the statement
except null or undefined. This means the ?? operator considers 0 valid and will return
it. It also considers false, NaN, and '' valid.
Try it out by refactoring DetailItem to use the nullish coalescing operator in place of
the OR operator.
...
function DetailItem({ items }) {
...
<div>
$
{(detailItem.salePrice || detailItem.price).toFixed(2)}
{(detailItem.salePrice ?? detailItem.price).toFixed(2)}
</div>
...
Save your file and check your browser to see what happened (Figure 10.15, “Free
coffee!”).
If you ever need to verify your assumptions about logical statements, you
can do that in the DevTools Console tab.
Open the Console tab and place the cursor next to the blue arrow
(Figure 10.16, “Console tab arrow”).
Then type out what you want to test, such as !!0 to see if 0 is truthy or
falsy. You will see a preview of the result in light colors. Press Return to
execute the line, and the result will darken.
Enter a few more tests, such as !!'' to check whether an empty string is
truthy or falsy. You can also test statements such as 0 || 1 and 0 ?? 1 and
compare the results (Figure 10.17, “Testing values in the console”).
And you can use fragments when you need to group elements together but
want to avoid unnecessary nesting and complexity.
You are making great progress with Code Café. In the next chapter, you will
learn about a new hook, useReducer, as you build out a cart for users to add
items to.
For the More Curious: Truthy and Falsy Values
For more examples of truthy and falsy values, check out
developer.mozilla.org/en-US/docs/Glossary/truthy
developer.mozilla.org/en-US/docs/Glossary/falsy
Silver Challenge: Promote Sale Items
To promote sale items, add <div>On Sale!</div> above the image if an
item has a sale price.
Remember that salePrice can be 0, which is falsy, so you will want your
conditional to be more specific than just a truthy/falsy check.
Chapter 11. useReducer
In Chapter 4, State, you used the useState hook to access a component’s
state. In this chapter, you will explore another React hook: useReducer,
which lets you set, modify, and access a state variable within a component.
You will take advantage of useReducer to add a shopping cart to Code Café
and manage its ongoing value.
If you have previously used Redux, a third-party library for managing state,
some of the patterns you see in this chapter might be familiar. However, you
do not need any prior knowledge of or experience with Redux to use the
useReducer hook.
By the end of this chapter, users will be able to add items to a cart, and a new
cart icon in the header will display the number of items in the cart
(Figure 11.1, “Code Café’s header at the end of the chapter”).
useReducer vs useState
In a moment, you will use the useReducer hook to add a new piece of state,
called cart.
Why use useReducer instead of useState? The short answer is that useState
is better for simple state management, while useReducer is better for
complex state management.
Users expect several functions from a shopping cart, such as adding an item,
removing an item, and emptying the cart.
With the useState hook, you have access to one method to update the value
of the state. Each time you use that method, you overwrite the entire state
value. When you need to interact with the state in multiple ways, such as for
different cart actions, things can get messy.
useReducer is also useful when the next state depends on the previous state,
such as when you want to update the quantity of an existing item in the cart.
It is OK if this does not all make sense right now. This chapter will walk you
through the useReducer hook and give you some practice so you can see how
it works.
Implementing useReducer
The useReducer hook accepts two arguments: a reducer function and an initial state
value. To keep your code organized, you will create these in a separate reducer
directory and file.
Add a new folder in the src directory called reducers, then create a
reducers/cartReducer.js file to export the two variables you will need:
initialCartState and the cartReducer function.
The first variable is initialCartState. The cart state will be an array of item
objects, where the key is the itemId and the value is the quantity of that item in the
cart. The initial state is an empty array, because there are no items in the cart yet.
The second variable is cartReducer. Do not worry about understanding what the
cartReducer function is doing yet. You will explore this in more detail shortly.
Now that you have these two values, add the useReducer hook to your application.
useEffect(() => {
...
You pass the useReducer hook the two values you just created – the reducer
function and the initial state value. useReducer returns an array with two values,
which you access using array destructuring.
As with useState, the first value of the returned array is the current state value.
Here, you name the state cart.
The second value in the returned array is the dispatch function. This is a special
function that useReducer creates. To update the state value, you will call the
dispatch function and pass it an action object.
Because you are not using the values of cart and dispatch yet, you add eslint-
disable-next-line no-unused-vars in a comment above the line where you
define them. This will stop ESLint from applying the no-unused-vars rule and
complaining about these unused variables until you make use of them.
The Reducer Function
Now let’s take a closer look at the cartReducer function you created in
cartReducer.js.
The reducer function receives two arguments: state and action. state is
the current state of your hook. action – the same object given to the
dispatch function – contains the type of action that you want your reducer
to perform.
The reducer also handles a default case. You use this to ensure that the code
throws an error if there is no matching action type. This will alert you or
other developers using the reducer that there is a mismatch between the
supported actions and the action type passed to the dispatch function.
return [
...state,
{ id: action.itemId, quantity: 1 },
];
JavaScript spread syntax creates a shallow copy of state, and the item
being added is appended to the new array.
Reducers should never mutate state directly. This is because React uses
reference equality to determine when to re-render, instead of looking deeply
into objects. Although mutating the initial state value might seem to work
as expected, it usually causes hard-to-debug issues down the road.
Instead, use spread or similar syntax to create a new state object. This will
create a new reference and trigger React to re-render when state changes.
Updating the Quantity
The action type add inserts the itemId in the array and sets the quantity to 1. But what
happens if the item is already in the cart? In that case, add should update the quantity of the
existing item instead.
Update the reducer function to handle the second case using a conditional expression.
const findItem = (cart, itemId) => cart.find((item) => item.itemId === itemId);
In your if statement, you use the findItem method to check whether the item already exists
in the cart. If the item is present, you create a new, updated array of items using map. Within
the map, the ternary increments the quantity of the matching item and leaves all the other
items as they are. And you avoid mutating the state directly by using spread syntax again for
the state change.
Before adding more actions to cartReducer, make your code easier to maintain by
refactoring the reducer to use variables for action types instead of strings.
const findItem = (cart, itemId) => cart.find((item) => item.itemId === itemId);
Now you can use the exported CartTypes variable in the reducer and in the dispatch
function.
Replacing strings with variables allows you to take advantage of your editor’s code
completion tools, which will greatly reduce the likelihood of spelling or syntax errors. It also
provides documentation for the available reducer actions.
Displaying Information in the Header
Now Code Café has a cart, and the cart can hold items – these are big steps. But at the
moment, the user gets no feedback to let them know that items are in the cart. For that
matter, they have no way to select items to add to the cart.
Next you will add a shopping cart icon to the header, along with the number of items in
the cart. This will let you easily see when an item has been added to the cart, without
having to use the DevTools. When that is complete, you will add the functionality to
actually put items in the cart.
You should already have cart.svg under src/images. If you do not, copy it from your
downloaded resources file.
Now, to access the current value of the cart state object, pass cart to Header as a prop.
...
function App() {
...
return (
<Router>
<Header />
<Header cart={cart} />
{items.length === 0
...
Add the cart icon and badge to Header, using the class names defined in Header.css.
The cart icon will also link to a separate cart page, which you will build out soon.
function Header() {
function Header({ cart }) {
const cartQuantity = cart.reduce((acc, item) => acc + item.quantity, 0);
return (
<header className="header-component">
<Link to="/">
<img src={CoffeeLogo} alt="coffee logo" />
<h1>Code Café</h1>
</Link>
<div className="menu">
<Link to="#todo">
<img src={CartIcon} alt="Cart" />
<div className="badge">{cartQuantity}</div>
</Link>
</div>
</header>
);
}
Header.propTypes = {
cart: PropTypes.arrayOf(PropTypes.shape({
itemId: PropTypes.string.isRequired,
quantity: PropTypes.number.isRequired,
})).isRequired,
};
You use the Array.reduce method to evaluate the quantity of items in the cart for the
cartQuantity variable. Although its name is similar, this method is not related to the
useReducer hook or its associated reducer function. The reduce method allows you to
reduce the data in an array to a single value – in this case, the total number of items in
the cart.
Save your files and take a look at your browser. You have a shiny new cart icon and
badge (Figure 11.2, “Cart icon”)!
This will give users instant feedback when they add items to the cart, which is the
functionality you will add next.
Adding Items to the Cart
The user should be able to add items to the cart from a button on each item’s details
page. For this to happen, DetailItem needs access to the dispatch function.
Pass dispatch as a prop to the DetailItem component in App.js. Also, now that you
are using both of the values from useReducer, you no longer need to disable the ESLint
warning about unused variables.
...
function App() {
const [items, setItems] = useState([]);
// eslint-disable-next-line no-unused-vars
const [cart, dispatch] = useReducer(cartReducer, initialCartState);
...
return (
<Route path="/details" element={<Details items={items} />}>
<Route path=":id" element={<DetailItem items={items} />} />
<Route
path=":id"
element={<DetailItem items={items} dispatch={dispatch} />}
/>
<Route index element={<div>No Item Selected</div>} />
...
Next, open DetailItem.js. Add the dispatch function to the component props. Then
add a new Add to Cart button that calls the dispatch function when clicked.
...
import './DetailItem.css';
import { CartTypes } from '../reducers/cartReducer';
return (
...
<div>
$
{(detailItem.salePrice ?? detailItem.price).toFixed(2)}
</div>
<button
type="button"
onClick={addItemToCart}
>
Add to Cart
</button>
</>
...
DetailItem.propTypes = {
dispatch: PropTypes.func.isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
...
You saw the onClick attribute in Ottergram; it is how you instruct React to listen for
click events. When the user clicks the Add to Cart button, React calls the dispatch
function with an object containing the action type and the itemId.
Internally, useReducer then calls cartReducer, passing it the current state of the
reducer as the first argument and the object passed to dispatch as the second argument.
Finally, the value that cartReducer returns becomes the new state of your reducer
(Figure 11.3, “Reducer flow”).
Action creators are a pattern you can use with useReducer to help solve this issue. They also
help reduce coupling between the reducer and the component using the reducer.
Action creators are essentially helper functions that call the dispatch function with a specific
action and the other necessary properties. Instead of calling dispatch directly, a component
instead calls the action creator.
Refactor App.js to include an action creator for adding an item to the cart. Pass the new function
to DetailItem as a prop, instead of passing dispatch. This will decouple DetailItem from the
cart reducer and make it clear that adding an item to the cart requires an itemId.
...
import NotFound from './components/NotFound';
import { cartReducer, initialCartState } from './reducers/cartReducer';
import { cartReducer, CartTypes, initialCartState } from './reducers/cartReducer';
function App() {
const [items, setItems] = useState([]);
const [cart, dispatch] = useReducer(cartReducer, initialCartState);
const addToCart = (itemId) => dispatch({ type: CartTypes.ADD, itemId });
...
return (
...
<Route
path=":id"
element={<DetailItem items={items} dispatch={dispatch} />}
element={<DetailItem items={items} addToCart={addToCart} />}
/>
...
The addToCart helper function allows components to add items to the cart without needing to
know about the reducer. This decouples the components from the implementation of the reducer.
Now update the Add to Cart button’s onClick handler to call the addToCart function instead of
dispatch.
...
import './DetailItem.css';
import { CartTypes } from '../reducers/cartReducer';
Save your files. In the browser, try adding items to the cart again. After refactoring, your code
should work as before.
Action creators are optional, so you can decide if they make sense for your app. Generally, action
creators are most useful for avoiding coupling between a component and the reducer and for
documenting the required properties for a given action. Action creators are less helpful when you
have a component that uses many actions from a reducer, because the component is naturally
more coupled to the reducer.
Conclusion
React provides the useState and useReducer hooks for managing state
inside your components. Each hook has its own use case: You generally use
useState to manage relatively simple state and useReducer to manage
more complex state.
In this chapter, you used the useReducer hook and your own reducer
function to set up a cart for Code Café that users can add items to. In the
next chapter, you will allow users to view and edit the cart.
Chapter 12. Editing the Cart
Developing Code Café has given you experience with building functional
components. You have also added state to keep track of the user’s cart and are
using the useReducer hook to manage that state.
In this chapter, you will create a route for users to view and edit the cart. This
chapter uses concepts you already know, giving you the opportunity to practice your
skills as you expand your app.
Try to write each new piece of code without first looking at the snippets we provide.
When you finish, compare your code with ours. You might do some things
differently from how we do them – that is fine, as long as your app behaves as
expected. But we do recommend that you edit your code to match ours before
moving forward to make sure you do not run into compatibility issues later.
Figure 12.1, “Completed cart” shows what your cart page will look like at the end
of the chapter:
Create a new Cart component in your components directory. Begin with a simple
component that returns a header with the text “Your Cart.” If you have not already
done so, copy the Cart.css stylesheet from your downloaded resources file into
your components directory. Then import it in the Cart component.
Finally, display the Cart component when the user visits the route /cart. See if you
can do this without looking at the solution below.
import './Cart.css';
function Cart() {
return (
<div className="cart-component">
<h2>Your Cart</h2>
</div>
);
}
...
import DetailItem from './components/DetailItem';
import Cart from './components/Cart';
import Details from './components/Details';
...
function App() {
...
<Routes>
<Route path="/cart" element={<Cart />} />
<Route path="/details" element={<Details items={items} />}>
...
Save your files and visit http://localhost:3000/cart in your browser to see your new
cart page (Figure 12.2, “Cart page”).
Example 12.3. Updating the Link for the cart icon (Header.js)
...
function Header({ cart }) {
...
<div className="menu">
<Link to="#todo">
<Link to="/cart">
<img src={CartIcon} alt="Cart" />
...
Save your file. In your browser, click Code Café in the header to navigate home,
then click the cart icon in the header to return to the cart page.
Viewing the Cart’s Contents
Now that users can access the cart page, you need to display the cart’s
contents. To do this, you must share the cart state with the Cart component.
The cart state that the useReducer hook manages is stored in the App
component. Should you move the cart state to Cart?
You should not, because you need access to the cart state in all of the
following:
useReducer does not inherently create global state. To share cart and its
related dispatch function with all three of these components, you must
create the state in a parent of the components. In Code Café, the closest
parent component is App.
Before you add the cart state to the Cart component, let’s explore how the
state behaves in your current code.
In your browser, open the DevTools. In the Components tab, select the App
component (Figure 12.3, “App component in the DevTools”).
The value for Reducer is an empty array. App calls useReducer one time, to
create the cart state and the dispatch function, so the Reducer you see in
the Components tab represents the cart state.
If your app used multiple reducer hooks, the Components tab would show
each one. To get the most benefit from the DevTools information, you need to
know which instance of a hook you want to explore.
Click the magic wand icon to the right of the hooks (Figure 12.4, “Parsing the
hook names”).
The DevTools add hook names to each hook displayed. This can be valuable
when you have multiple hooks and want to quickly differentiate them
(Figure 12.5, “Displaying the hook names”).
Now you can see that the name of the reducer value is cart.
Next, add items to the cart by clicking the Add to Cart button on your site.
Watch the state change in the DevTools as the items are added to cart
(Figure 12.6, “Displaying the hook state”).
React state does not persist across refreshes. To add persistence, you can use
browser storage, which you will implement in Chapter 14, Local Storage and
useRef.
Being able to see the current state in the DevTools is useful for making sure
the app behaves as expected during development and for debugging the app if
it does not.
Now it is time to provide the Cart component with updated state information.
Displaying the Cart Contents
Get started by passing the cart prop to the Cart component.
...
function App() {
...
<Routes>
<Route path="/cart" element={<Cart />} />
<Route path="/cart" element={<Cart cart={cart} />} />
<Route path="/details" element={<Details items={items} />}>
...
See if you can make the next changes without looking at the code. You need to import the
cart prop into the Cart component and add the corresponding propTypes. (Remember that
the cart prop is an array. Each element in the array is an object with the keys itemId and
quantity.)
You can use table elements to display data in React, just like in HTML. Use a table to display
the Cart data in rows, with the item quantity in the first column and the item ID in the second
column.
function Cart() {
function Cart({ cart }) {
return (
<div className="cart-component">
<h2>Your Cart</h2>
<table>
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
</tr>
</thead>
<tbody>
{cart.map((item) => (
<tr key={item.itemId}>
<td>{item.quantity}</td>
<td>{item.itemId}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Cart.propTypes = {
cart: PropTypes.arrayOf(PropTypes.shape({
itemId: PropTypes.string.isRequired,
quantity: PropTypes.number.isRequired,
})).isRequired,
};
In the browser, add some items to the cart. Then click the cart icon to navigate to the cart
page and see its contents (Figure 12.7, “Displaying the cart contents”).
It would be better to display the item title instead of the itemId. You should also show the
item price.
Right now, the cart object stores only the itemId of each item. Should you store title and
price in cart as well? Actually, no.
An item’s price and even its name might change periodically. If you store only the itemId –
and find the title and price later, when the user visits the cart page – you do not have to worry
about updating every user’s cart with the latest title and price when they change.
...
function App() {
...
<Routes>
<Route path="/cart" element={<Cart cart={cart} />} />
<Route path="/cart" element={<Cart cart={cart} items={items} />} />
<Route path="/details" element={<Details items={items} />}>
...
Now add items and the corresponding prop types to the Cart component, using the ItemType
you created in Chapter 6, Prop Types. Use the array find method to display the title instead
of the itemId of each item in the cart.
Save your file and take a look at the changes in your browser (Figure 12.8, “Item titles in the
cart”).
Figure 12.8. Item titles in the cart
Next you will add the item’s price to a third column in the table. The price shown should be
the total price, taking into account the quantity of the item in the cart. Also, recall that some
items have a salePrice field that takes precedence over the regular price field.
One option for handling the price column is to call the find method again to find the item’s
price, just like you did with the title. This approach would work fine for this application,
because of the small number of items in the items array.
But imagine if there were hundreds or thousands of possible items – which could easily be
the case in a production app. Calling find multiple times to iterate through so many items,
every time the component rendered or re-rendered, would be an expensive task.
You need a new variable to save the matching item. Though you can create new variables
within cart.map, it adds complexity to the JSX, making it harder to understand. Instead of
creating new inline variables, it is a better idea to create a new component.
You will create a new CartRow component to represent a row of data in the cart. This
component will take two props: a cartItem and the items array.
Create a new file called CartRow.js and build the new component.
return (
<tr>
<td>{cartItem.quantity}</td>
<td>{item.title}</td>
<td>
$
{((item.salePrice ?? item.price) * cartItem.quantity).toFixed(2)}
</td>
</tr>
);
}
CartRow.propTypes = {
cartItem: PropTypes.shape({
itemId: PropTypes.string.isRequired,
quantity: PropTypes.number.isRequired,
}).isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
};
Now refactor your table in Cart so that map returns the new component with the needed
props. Remember to keep the key attribute for the child components of map.
The first step is to update the cart reducer to support a new REMOVE action type. This action
type removes an item from the cart by removing it from the array in state.
...
export const CartTypes = {
ADD: 'ADD',
REMOVE: 'REMOVE',
};
...
export const cartReducer = (state, action) => {
...
return [
...
];
case CartTypes.REMOVE:
return state.filter((item) => item.itemId !== action.itemId);
default:
...
Remember from Chapter 11, useReducer that reducer functions should be pure – they should
never mutate state directly. For example, the Array.splice method operates on arrays in
place, so if you were to use it, you would first need to clone the array to avoid mutating the
incoming state.
Array.filter returns a new array with only the elements that passed the test (in other words,
those that returned true). Though using filter is not the only way to accomplish this task, it
is a good single-line solution that avoids mutating state.
The button to remove an item will be in the CartRow component. When the button is clicked,
it will call the dispatch function with the REMOVE action type you just created.
The dispatch function lives in App.js, and it needs to get to CartRow, which is the child of
Cart. You can pass props from a component only to its direct children, so you will need to
pass dispatch as a prop first from App to Cart, then from Cart to CartRow.
You can. But the Cart component is strongly coupled to the cart reducer by the shape of the
data and will eventually use several reducer actions. Passing the dispatch function as a prop
eliminates the need to pass each action creator as a separate prop. However, if you prefer the
style of action creators, it is perfectly valid to use one here.
Now add dispatch to the prop signature for Cart. Finally, pass it as a prop to CartRow.
...
function Cart({ cart, items }) {
function Cart({ cart, dispatch, items }) {
...
<tbody>
{cart.map((item) => (
<CartRow key={item.itemId} cartItem={item} items={items} />
<CartRow
key={item.itemId}
cartItem={item}
items={items}
dispatch={dispatch}
/>
))}
</tbody>
...
Cart.propTypes = {
cart: PropTypes.arrayOf(PropTypes.shape({
...
})).isRequired,
dispatch: PropTypes.func.isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
...
Now CartRow has access to the dispatch function, and you can add the button that will
depend on dispatch.
return (
...
</td>
<td>
<button
type="button"
onClick={removeItemFromCart}
>
X
</button>
</td>
</tr>
);
}
CartRow.propTypes = {
cartItem: PropTypes.shape({
...
}).isRequired,
dispatch: PropTypes.func.isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
...
Rather than creating a variable, you could call dispatch inline in onClick. In that case, you
would need to use an anonymous arrow function, like () => dispatch(...). This is because
React evaluates the content inside curly braces on render, so onClick={dispatch(...)}
would invoke the dispatch function on render and immediately remove all the items from
the cart.
Save all your files and check out your work in the browser (Figure 12.10, “Removing items
from the cart”).
This is also what you see when you visit the page before adding any items to
the cart. A better user experience would be to show a message letting the user
know that the cart is empty.
Refactor the cart, using conditional rendering to show the user a message
instead of the table when the cart is empty. (You will need to reindent the
<table> opening and closing tags and everything in between. You can use
ESLint’s auto-fix feature to take care of the indentation for you once you add
the new code.)
...
function Cart({ cart, dispatch, items }) {
return (
<div className="cart-component">
<h2>Your Cart</h2>
{cart.length === 0 ? (
<div>Your cart is empty.</div>
) : (
<table>
...
</table>
)}
</div>
);
...
Save your file and visit your cart in the browser. As long as it is empty, you
should see your new message (Figure 12.12, “Empty cart”).
Compute the subtotal for the cart. For each item, multiply its price by its
quantity, then sum all the calculated values. (Remember that an item
might have a salePrice that you should use in place of price.)
Display the subtotal in a <div> below the table, making sure that it always
has exactly two digits after the decimal point. (You will need a way to
have two child elements. How did you do this before?)
Make the changes in Cart. (Again, you will need to reindent the newly nested
elements.)
...
function Cart({ cart, dispatch, items }) {
const subTotal = cart.reduce((acc, item) => {
const detailItem = items.find((i) => i.itemId === item.itemId);
const itemPrice = detailItem.salePrice ?? detailItem.price;
return item.quantity * itemPrice + acc;
}, 0);
return (
<div className="cart-component">
<h2>Your Cart</h2>
{cart.length === 0 ? (
<div>Your cart is empty.</div>
) : (
<>
<table>
...
</table>
<div>
Subtotal: $
{subTotal.toFixed(2)}
</div>
</>
)}
...
The JavaScript array reduce function provides a nice way to get the subtotal
from all the cart items. But again, there are many ways to compute the subtotal.
The ternary statement returns a JSX expression, which must have one parent
element. This code uses a React fragment to wrap the <table> and <div>
elements so that only one root element returns from either side of the ternary.
Save your file, add some items to your cart, and check out the subtotal
(Figure 12.13, “Drinks for two”).
In the next chapter, you will learn more about forms and input elements in
React. You will add a form to the cart page so users can check out and place
their orders.
Bronze Challenge: Increasing the Quantity
What happens if a user gets to the cart and remembers that they wanted to
add a second coffee for a friend? Right now, they have to go back home,
click the coffee thumbnail, and click Add to Cart again.
Add a + (plus) button so the user can increase the quantity of an item by 1.
You can use the existing ADD action type with the dispatch function.
Silver Challenge: Decreasing the Quantity
What happens if a user accidentally orders two coffees instead of one?
Right now, they have to click the remove button, which removes both
coffees. Then, they have to go back to the coffee details page to add one
coffee again. This is not ideal.
Add a - (minus) button so the user can decrease the quantity of an item by
1. You will need to add a new DECREASE reducer type, which has a special
case: If the user has only 1 of the item in the cart, decreasing the quantity
should remove it from the cart, rather than leaving it in the cart with a
quantity of 0.
Chapter 13. Forms
Code Café’s users can add tasty treats to the cart and update it to have exactly
the items they want. Next, they need to be able to submit their orders.
When the user places an order, you need to collect certain information, such
as the customer’s name. To do this, you will add a form to the cart page.
Forms allow users to input and submit information. If you are familiar with
HTML forms, you will probably recognize the basic elements used to build
forms in React. Because these elements are interactive and used to capture
input, there are some caveats to working with them in React. This chapter
will explore ways to implement form elements in your application, including
managing state.
When the user is ready to place an order, they will need to enter their name
and their ZIP code (to compute tax). They will also have the option to enter a
phone number. To keep things simple, you will add the checkout form right
on the cart page, rather than creating a new route (Figure 13.1, “Completed
checkout form”).
...
function Cart({ cart, dispatch, items }) {
...
<div>
Subtotal: $
{subTotal.toFixed(2)}
</div>
<h2>Checkout</h2>
<form>
<label htmlFor="name">
Name
<input
id="name"
type="text"
/>
</label>
</form>
</>
...
There are two ways to bind a label and an element together in React.
One way is to use the htmlFor attribute. In HTML, you use the for attribute
to associate labels with elements. Because for is a reserved word in
JavaScript, JSX uses htmlFor instead. The label is bound to the input using
the input’s id.
Save your file. To see the form in your browser, add at least one item to the
cart (Figure 13.2, “New checkout form”).
Next, take a look at the form elements in the DevTools Elements tab
(Figure 13.3, “Inspecting the form elements”).
Add two more inputs with labels to the form, one for the phone number and
one for the ZIP code, following the pattern you just used for the name input.
...
function Cart({ cart, dispatch, items }) {
...
<label htmlFor="name">
...
</label>
<label htmlFor="phone">
Phone Number
<input
id="phone"
type="tel"
/>
</label>
<label htmlFor="zipcode">
ZIP Code
<input
id="zipcode"
type="text"
maxLength="5"
inputMode="numeric"
/>
</label>
</form>
...
There are several attributes you can add to JSX elements, just like HTML
elements.
In addition to the id attribute, each input also has a type attribute. This
attribute defines the input behavior. The tel type indicates an input for a
phone number. It does not add phone number formatting, such as dashes –
you will add that yourself later in this chapter. But it cues the browser to offer
to autofill the field if the user has phone numbers stored in their profile, and
on mobile browsers it triggers the numeric keyboard.
In both HTML and JSX, the default type value is text, which you use for the
name and zipcode inputs. Because it is the default, you could have left the
type specification out. However, it is a best practice to include the type for
all inputs to keep your code clear.
The extra attributes that you added to zipcode are inputMode and maxLength.
Though inputMode does not restrict the type of characters that the user can
enter into an input, it does specify which keyboard a browser should use.
maxLength restricts the length of the input, to five characters in this case.
Now, add the last piece of the form: a button to place the order. A button does
not need a label, so you do not need to worry about binding it to one.
...
function Cart({ cart, dispatch, items }) {
...
<label htmlFor="zipcode">
...
</label>
<button type="submit">
Order Now
</button>
</form>
...
The button has the type submit. This is the default type for buttons associated
with a form in HTML and JSX. When the user clicks the button, the form’s
onSubmit event is triggered.
Save your file and take another look at the cart page (Figure 13.4, “Cart with
checkout form”). (If the cart is empty, add an item to it.)
The cart is empty and the URL ends in a ?. This is due to the browser’s native
form submission. By default, the browser submits the form values as a new
GET request to the current URL, then displays the result. (Because your form
fields do not have name properties, the values do not show in the URL.)
In most scenarios, you will implement your own behavior for form
submission. React allows you to listen for form submission events with a
form’s onSubmit prop. You pass the event as the first argument to the
onSubmit callback function. (This is similar to the onClick attribute you used
for buttons in Ottergram.)
...
function Cart({ cart, dispatch, items }) {
const subTotal = cart.reduce((acc, item) => {
...
}, 0);
return (
...
<div>
Subtotal: $
{subTotal.toFixed(2)}
</div>
<h2>Checkout</h2>
<form>
<form onSubmit={submitOrder}>
<label htmlFor="name">
...
Save your file. Back in the browser, add items to your cart and navigate to the
cart page. Try clicking the Order Now button again. The page no longer
reloads, because you are preventing the default behavior.
Value from Inputs
In the DevTools, open the console to see the output from the event handler
function. Look for the SyntheticBaseEvent entry and expand its contents
(Figure 13.6, “SyntheticBaseEvent in the console log”).
Expand the contents of the target property (Figure 13.7, “Event target”).
Let’s update the console.log in the onSubmit handler to print the value of
each input using the id.
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = (event) => {
event.preventDefault();
console.log(event);
console.log('name: ', event.target.name.value);
console.log('phone: ', event.target.phone.value);
console.log('zipcode: ', event.target.zipcode.value);
};
...
Save your file and return to the browser. Fill in values for each of the form
fields and click the Order Now button. Check the end of the console output to
ensure that the values are printing as expected (Figure 13.8, “Input values in
the console log”).
In simple forms, such as the one you have here, this might be sufficient. But
what if you wanted to add behavior such as text formatting or validation?
Although you could do that, it would be tricky and would involve plain
JavaScript – not an ideal solution.
To experiment with this, add the value attribute to the name input element
and set it to "M. Mouse".
...
function Cart({ cart, dispatch, items }) {
...
<label htmlFor="name">
Name
<input
id="name"
type="text"
value="M. Mouse"
/>
...
Save your file, head back to the cart page, and try to edit the value in the Name
field. Even though the input is not readonly or disabled, you cannot edit it.
This is because React uses one-way data binding. React ensures that the value
returned from the component matches what renders. When you try to change
the value, React notices the change and immediately reverts it to match the
value from the component. The result is that the UI is the same, because the
value that the component returns, "M. Mouse", does not change.
So how do you tell React to update the value when the user makes a change?
The error gives you helpful information on how to make this form editable
again: You can add an onChange handler to an individual form element. In the
onChange handler, you tell React what to do when the user edits the input
value.
onChange
The first thing you need is a state value to keep track of the name input. Add this
using the useState hook.
In the name input element, use the onChange attribute to update the state value
each time the user makes an edit. Replace the hardcoded value with the new
state value.
onChange (like onClick or onSubmit) passes the event to the event handler (the
arrow function you defined). The event contains the input’s new value in
event.target.value. You use that to update the state, which React passes as
the value to the element.
Each time the state name updates, React re-renders the component with the new
value.
Save your file, visit your form, and enter a name in the Name field. Nice – the
field is editable again.
Now open the Components tab in the DevTools and select the Cart component.
Under hooks, you can see the current value for the name state (Figure 13.10,
“name state in the DevTools”).
Next, follow the same pattern to store the values for the phone number and ZIP
code in state, as phone and zipCode.
See if you can make these changes without first looking at the solution below.
Example 13.8. Adding state for the phone number and ZIP code (Cart.js)
...
function Cart({ cart, dispatch, items }) {
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [zipCode, setZipCode] = useState('');
Save your file and confirm that you can enter a name, phone number, and ZIP
code and see all three state values in the DevTools.
Now you have local state variables that you can access and manage. This gives
you much more control over what information the form eventually submits.
Because of React’s declarative design approach, you need the loop of interaction
between onChange and value to give you control over editable values. Although
the declarative approach requires more typing than some other front-end
frameworks, the benefits outweigh this inconvenience.
Next, update the onSubmit handler to log the new state values in the console
when the form is submitted.
Example 13.9. Updating onSubmit (Cart.js)
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = (event) => {
event.preventDefault();
console.log('name: ', event.target.name.value);
console.log('name: ', name);
console.log('phone: ', event.target.phone.value);
console.log('phone: ', phone);
console.log('zipcode: ', event.target.zipcode.value);
console.log('zipcode: ', zipCode);
};
...
Calculating the Tax
Because you store your form’s values in state, now you can use those values in other
areas. Having easy-to-access values is a benefit of using controlled components.
To compute the total for the order, you need to add tax. For simplicity, assume that the
tax rate is the first digit of the ZIP code plus 1. Then compute and display the tax.
...
function Cart({ cart, dispatch, items }) {
...
const subTotal = cart.reduce((acc, item) => {
...
}, 0);
return (
...
<div>
Subtotal: $
{subTotal.toFixed(2)}
</div>
<div>
Tax: $
{tax.toFixed(2)}
</div>
<h2>Checkout</h2>
...
Save your file. Back on the cart page, fill in a ZIP code, and you will see the tax
display (Figure 13.11, “Cart page with tax”).
...
function Cart({ cart, dispatch, items }) {
...
const tax = subTotal * taxRate;
const total = subTotal + tax;
return (
...
<div>
Tax: $
{tax.toFixed(2)}
</div>
<div>
Total: $
{ total.toFixed(2) }
</div>
<h2>Checkout</h2>
...
Save your file and check out your browser to view the total (Figure 13.12,
“Displaying the total”).
There is another UI state you must handle. To see it, clear out the ZIP code field.
When no ZIP code is entered – such as when the user first visits the cart page – the
tax and total displayed are based on a 1% tax rate. Since the tax rate is actually
unknown, the page should not show the tax or total until the user enters their ZIP
code.
Use the local state value for zipCode to determine whether the tax and total should
render. If the user has entered a five-digit ZIP code, they should see the tax and the
total. If the ZIP code is missing or incomplete, the cart should display a message
telling the user what they need to do (enter their ZIP code) to see the total.
You will need to reindent the <div>s for the tax and the total.
...
function Cart({ cart, dispatch, items }) {
...
<div>
Subtotal: $
{subTotal.toFixed(2)}
</div>
{ zipCode.length === 5
? (
<>
<div>
Tax: $
{tax.toFixed(2)}
</div>
<div>
Total: $
{ total.toFixed(2) }
</div>
</>
) : (
<div className="warning">Enter ZIP Code to get total</div>
)}
<h2>Checkout</h2>
...
Save your file and test your changes in the browser (Figure 13.13, “ZIP code error”).
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = (event) => {
...
};
return (
...
<input
id="phone"
type="tel"
value={phone}
onChange={(event) => setPhone(event.target.value)}
onChange={(event) => setFormattedPhone(event.target.value)}
/>
...
Now the onChange function calls the helper function setFormattedPhone. This helper
function performs the logic to format the phone number and then calls setPhone to
update the state value with the newly formatted number. This logic is more complicated,
so the helper function exists outside the JSX.
Type in a phone number without dashes. The form will automatically insert the dashes
for you (Figure 13.14, “Formatted phone number”).
This is great! But it is not perfect: Try editing the middle of the phone number. Your
cursor will continually jump to the end of the input.
Although React and JavaScript allow you to work with the exact cursor position, it is
complicated. There are npm packages that can help you apply formatting and deal with
the cursor position. At the end of this chapter, there is a challenge to add a package for
handling this issue.
Form Validation
For Code Café to process an order, the shop needs the user’s name (for
identification) and ZIP code (for tax). The Order Now button should be enabled
only if the user has supplied the required information. On the other hand, the
phone number is not required, so the button should not depend on whether the
user has entered a phone number.
You can use the required HTML attribute with React form elements. Go ahead
and mark both the name and zipcode inputs as required.
...
function Cart({ cart, dispatch, items }) {
...
<input
id="name"
type="text"
value={name}
onChange={(event) => setName(event.target.value)}
required
/>
</label>
<label htmlFor="phone">
...
</label>
<label htmlFor="zipcode">
ZIP Code
<input
...
onChange={(event) => setZipCode(event.target.value)}
required
/>
...
React allows you to bind Boolean values to the HTML attributes. If the value is
truthy, React adds the attribute to the element. If it is falsy, React does not add
the attribute.
When you do not give a value, React assumes the value is true. So you could
also have written
required={true}
However, the Airbnb ESLint rule set requires that you omit true values.
Save your file and try to submit the form with one or both of the required fields
blank. The onSubmit handler does not fire. Instead, you get an alert to fill out
the empty field (Figure 13.15, “Required field alert”).
...
function Cart({ cart, dispatch, items }) {
...
const tax = subTotal * taxRate;
const total = subTotal + tax;
const isFormValid = zipCode.length === 5 && name.trim();
Save your file, visit the browser again, and confirm that the Order Now button is
disabled until you enter a name and ZIP code (Figure 13.16, “Disabled Order
Now button”).
Think about how far Code Café has come. Now users can add items to their
cart, see all the items in their cart, and review the total cost, including tax.
And as a React developer, you have built multiple components and used
two hooks for managing state: useState and useReducer.
In Chapter 15, Submitting Orders, you will allow users to submit their
orders.
But first, you will learn about another hook often used with form elements
and much more: useRef. You will also get more practice working with form
data.
Silver Challenge: Coupon Code
Add a coupon code field to the checkout form.
Code Café’s coupon codes are all uppercase. If the user types lowercase
characters, reformat them appropriately.
Gold Challenge: Maintaining the Cursor Position
in the Phone Number
Advanced number formatting and patterns, such as the ability to edit the
middle of a phone number, can require complex code. Most front-end
developers reach for a package that takes care of formatting for them.
You will need to install this new package with npm install --save
react-number-format and then replace the phone number <input> with
<PatternFormat>. Follow the link from the react-number-format page to
its full documentation. There, read the Pattern Format section to see how to
specify the input pattern. Also, look at the Props section. Instead of
onChange, react-number-format uses onValueChange and provides you
with an object, rather than the event, as the first parameter.
Gold Challenge: Setting the Quantity
There are other available form elements that you have not used in building
the checkout form. One of these is the <select> element.
Though the plus and minus buttons from the previous chapter’s challenge
are nice, you could also offer the user a way to set the quantity directly.
Instead of an <input> element (which has additional complexity when it is
blank), use a <select> element that lets the user choose their desired
quantity.
For an added challenge, remove the item when the quantity entered is 0.
Chapter 14. Local Storage and useRef
In the last chapter, you built a form for users to input their information and submit it. You also
used controlled components to manage the state of each form element.
In this chapter, you will use local storage to persist the values in the cart in case the user closes or
refreshes the browser. You will also use the form you created to learn about another React hook:
useRef.
The useRef hook creates and stores a mutable JavaScript object that persists through re-renders.
This means you can use useRef to store values that are not intended to trigger a re-render of the
component, such as a reference to a timer. A more common use case for this hook is storing a
reference to a React element, giving you direct access to the DOM node that the element creates.
This chapter will show you examples of both.
useRef is a specialized hook, so most production applications use it only in a couple of places.
This chapter will help you understand when to reach for useRef and when you might want
useState instead.
But before diving into useRef, you will work on persisting cart values across browser refreshes.
Local Storage
Right now, items in the cart disappear when you refresh the browser because the application
stores them only in local state. During development, this is frustrating because you must return to
the home screen and re-add items. And your users certainly will not want to lose all their items if
they refresh the browser.
Browsers can persist information using the Web Storage API. This API has mechanisms for both
local storage and session storage. Though both types of storage use key/value pairs and persist
through refreshes, local storage persists even if the browser closes and reopens, so it will work
best for storing Code Café’s cart.
There are several methods for interacting with local storage, including a setter method for storing
a value and a getter method for accessing it.
Open App.js and use the setter localStorage.setItem to store the cart value each time it
updates.
...
import { cartReducer, CartTypes, initialCartState } from './reducers/cartReducer';
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(cart));
}, [cart]);
useEffect(() => {
...
Recall from Chapter 8, Interacting with a Server that you use the useEffect hook to perform
side effects during the render cycle. Updating the application state, such as the value of cart,
causes App to re-render. React then evaluates useEffect’s dependency array to determine
whether it should execute the effect. If the values in the dependency array have changed, the
code in useEffect runs.
Here, you add useEffect with one value, cart, in its dependency array. Each time App re-
renders, React determines whether the value of cart has changed. If so, it runs the code inside
useEffect to set the cart value in local storage.
localStorage.setItem takes two arguments. The first is the key that identifies the data you are
storing. The second is the value you are storing. Because you can save only strings in local
storage, you use the JSON.stringify method to convert the value to a string before passing it to
setItem.
Save your file, then test the new code by adding several items to your cart. Open the DevTools.
In the Application tab, double-click Local Storage in the left pane to expand it. You will see the
current domain, http://localhost:3000. Select the domain to see the stored cart value
(Figure 14.1, “cart in local storage”).
useReducer gives you two ways to set the initial value. You have already seen one way: passing
the initial value as the second argument to useReducer. You can also pass a function as the third
argument to lazily create the initial state. Creating initial state lazily lets you avoid running
expensive calculations – such as reading and parsing local storage – on every render cycle.
Pass a function to useReducer that fetches the value of cart from local storage using
localStorage.getItem, and set that value as the hook’s initial value.
...
function App() {
const [items, setItems] = useState([]);
const [cart, dispatch] = useReducer(cartReducer, initialCartState);
const [cart, dispatch] = useReducer(
cartReducer,
initialCartState,
(initialState) => {
try {
const storedCart = JSON.parse(localStorage.getItem(storageKey));
return storedCart || initialState;
} catch (error) {
console.log('Error parsing cart', error);
return initialState;
}
},
);
const addToCart = (itemId) => dispatch({ type: CartTypes.ADD, itemId });
...
You use the new function as useReducer’s third argument to lazily initialize the state value. But
then what is the second argument, initialCartState, doing?
In useReducer, the function – which you pass as the third argument – automatically has access to
the initial state value – which you pass as the second argument. To use the initial state value
inside the function, you add a parameter (here called initialState) to the function definition.
The parameter’s name does not matter; its value is always useReducer’s second argument. Here,
the second argument is the variable initialCartState, whose value is an empty array, so
initialState is an empty array.
Earlier, you used JSON.stringify to ensure you were sending the cart value to local storage as a
string. Here, you use JSON.parse to read the stored cart value, parsing it as a JavaScript array or
object. Then you use the OR operator (||) to check whether this parsed value is truthy. If it is,
you return it. Otherwise, you return the initialState.
Wrapping the code in a try/catch block provides a fallback: In case of an error, the code returns
initialState. This prevents the application from crashing if there is a problem reading or
parsing the value from local storage.
Save your file. In the browser, make sure there are some items in your cart. In the DevTools’
Application tab, make sure you can see the cart value in the Local Storage section. Now refresh
the browser and navigate to the cart page. All your items are still in the cart.
useRef vs useState
Now that the cart data persists even if the browser closes, it is time to look at
useRef. Let’s start by exploring how useRef differs from useState.
Each time the state updates, the component re-renders to keep the UI in sync
with the state. Try this exercise to see these re-renders in action: In the Cart
component, add a new state variable that increases by 1 each time the user
clicks the Order Now button. Add a <div> below the checkout form that
displays the new state. While you are updating submitOrder, go ahead and
remove the console.log calls – you do not need them anymore.
Every time the state changes, the variable will update onscreen, so you will
know the component has re-rendered.
...
function Cart({ cart, dispatch, items }) {
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
const [zipCode, setZipCode] = useState('');
const [renderCounter, setRenderCounter] = useState(0);
Save your file. In the browser, add items to your cart and navigate to the cart
page. Fill in the name and ZIP code to enable the Order Now button.
Click the button several times to see what happens (Figure 14.2, “Testing
component re-renders with useState”).
Now refactor the experiment to use the useRef hook instead of useState.
One difference between useState and useRef is right in the names: useState
uses a state, and useRef uses a ref. Refs are objects that allow you to
reference a value such as a DOM node or another piece of data.
Add a console.log statement that logs the current value of the ref, using the
ref’s current property.
You access the ref object’s current value by using its current property.
Because objects are passed by reference in JavaScript, ref.current always
reflects the current value of the reference, even if ref is from an old render
cycle. Therefore, you should never destructure a ref or otherwise cache
ref.current in a variable.
Unlike useState, the useRef hook does not return a setter for updating the
relevant value. Instead, React updates the renderCounter.current value
directly, adding 1 each time you click the button.
Save your file. In the browser, open the DevTools’ Console tab, then test your
new code by clicking the Order Now button several times.
Each time you click the button, the ref value updates, and the new value
prints in the console. But the updated value does not trigger the component to
re-render. As a result, the RenderCounter value on the page stays at 0
(Figure 14.3, “Testing component re-renders with useRef”).
The useRef hook is a powerful tool that you should use only sparingly. In
most cases, you should store values in state to ensure that the component re-
renders as needed and the UI stays in sync with the data. However, refs are
useful for keeping up with references to timeouts you might need to clear or
API calls you might need to cancel.
Using setTimeout with useRef
Each month, Code Café recognizes two employees for their hard work and gives them
free orders. To determine who is eligible, the API provides the endpoint
/api/employees/isEmployeeOfTheMonth, which takes a name as the query string. The
endpoint checks this name against a list of the current employees of the month and
returns an object that looks like { isEmployeeOfTheMonth: boolean }.
Loren and Ashley are the current employees of the month for Code Café, so their
names will return { isEmployeeOfTheMonth: true }. All other strings will return {
isEmployeeOfTheMonth: false }.
Create a function to handle change events for the Name field. As the user types their
name in the Name field, make a GET request to the endpoint to determine whether the
name belongs to an eligible employee. Set the result in a new piece of state. If the result
is true, change the cart subtotal to 0.
The new onNameChange function allows you to trigger events when the user is typing,
just like you did when formatting the phone number before storing it in state. In this
case, the additional event is a network call to
/api/employees/isEmployeeOfTheMonth. The query string ?name=${newValue} is
appended to the end of the request.
Save your file and return to your browser. In the DevTools, navigate to the Network
tab. Begin typing in the Name field. Each time you make a change to the string, the app
makes a new network request with the new string (Figure 14.4, “Network requests
made while typing”).
What if the app makes the request only when the user takes a short break from typing?
This is called debouncing and is often how search inputs work. Debouncing actions can
dramatically reduce the number of network calls your app makes.
...
function Cart({ cart, dispatch, items }) {
...
const onNameChange = (newName) => {
setName(newName);
setTimeout(() => {
axios
.get(`/api/employees/isEmployeeOfTheMonth?name=${newName}`)
.then((response) => setIsEmployeeOfTheMonth(
response?.data?.isEmployeeOfTheMonth,
))
.catch(console.error);
}, 300);
};
...
Save the file. In the browser, type a name into the checkout form. Watch the results in
the DevTools.
Although the requests are being delayed, each input change still triggers a new request.
What is going on?
setTimeout is a global JavaScript function. To use it, you pass it a function to execute
and a delay in milliseconds, as you do here. setTimeout creates a timer for the delay
and executes the provided function only when the timer has expired. In this case, each
call to onNameChange creates a new timer – so the provided function runs once for each
timer that expires.
To get debouncing working properly, you need to clear any previous timers before
creating the next one.
setTimeout returns a value that identifies the current timer. Using another global
JavaScript function called clearTimeout, you can use this value to stop the timer.
This is where useRef comes in handy. You can store the value of the setTimeout
function in useRef, because updating that value should not trigger any re-renders in the
application.
Try it out by adding a new ref. When the name value changes, check the ref’s current
value to see if a previous timer already exists. If it does, clear the timer using
clearTimeout. Then set the ref equal to a new setTimeout timer.
After saving your file, clear the network requests by clicking the ⊘ button in the
Network tab’s menu bar. Then type in the Name field again. Now only one network
request occurs, when you finish typing. (If you pause and begin typing again, you will
see multiple requests.)
If you entered the name of an eligible employee, the subtotal and subsequent
calculations will all show as $0.00 (Figure 14.5, “Free orders for an employee of the
month”).
Take a look at the checkout form you built. Since a phone number has a fixed
number of digits, it would be nice if the user’s cursor automatically moved from
the phone number input to the ZIP code input once they have entered the last
digit of the phone number.
To advance the focus to the ZIP code input, you will need to call focus on the
related DOM node. To access the DOM node, you will use useRef.
Each element has a ref prop to which you can pass the ref object that useRef
returns. Create a ref for the zipCode input:
...
function Cart({ cart, dispatch, items }) {
...
const debounceRef = useRef(null);
const zipRef = useRef(null);
...
return (
...
<input
id="zipcode"
...
onChange={(event) => setZipCode(event.target.value)}
required
ref={zipRef}
/>
...
Since zipRef is a ref object, React sets the object’s current property to the
relevant DOM node when the component renders. This allows you to access the
DOM node using zipRef.current.
In the setFormattedPhone function, check whether the phone number has the
required number of digits. If it does, call the focus method on the ZIP code
input, using the current value of the ref object.
...
function Cart({ cart, dispatch, items }) {
...
const setFormattedPhone = (newNumber) => {
...
} else if (digits.length > 6) {
formatted = `${formatted}-${digits.substring(6, 10)}`;
}
In the checkout form, enter 10 digits in the phone number input. As you enter
the 10th digit, the focus will advance to the ZIP code input, highlighting the
input and moving your cursor (Figure 14.6, “ZIP code input with focus”).
To make Code Café more accessible, add instructions to let the user know exactly what
will happen when the phone number is complete.
...
function Cart({ cart, dispatch, items }) {
...
return (
...
<input
id="phone"
...
onChange={(event) => setFormattedPhone(event.target.value)}
aria-label="Enter your phone number.
After a phone number is entered,
you will automatically be moved to the next field."
/>
...
The aria-label gives accessibility information to screen readers. Now users know they
should expect the focus to change after they enter the phone number.
Conclusion
useRef is an important tool that gives you added control over the behavior
of your components. It allows you to keep track of a value without
triggering component re-renders and lets you directly access DOM
elements.
In the next chapter, you will integrate your form with the server so that
users can submit their orders. If you feel like you need more practice with
useRef before moving on, check out the challenge below.
Silver Challenge: Returning Focus to the Name
Input
You programmatically advance focus when the user finishes entering their
phone number. Now, do the same with the ZIP code, but only if the user has
neglected to enter their name: When the user finishes inputting the five-
digit ZIP code, check whether the name is blank. If so, move focus to the
name input. Otherwise, leave the focus alone.
(Why not move focus to the Order Now button? Pressing Return in any
form field submits the form, so many browsers will not move focus to a
submit button.)
Hint: You will likely want to create a separate function to handle the
onChange event for the ZIP code, as the logic will be difficult to fit in
onClick.
Chapter 15. Submitting Orders
Although Code Café’s users can build an order, they cannot actually send it to
the café.
By the end of this chapter, you will store orders on the server, ready for the
café staff to fulfill (Figure 15.1, “Orders submitted to the server”).
useEffect(() => {
axios.get('/api/items')
.then((result) => setItems(result.data))
.catch(console.error);
}, []);
The request you will make to submit the form will be different, because its
trigger will be a direct user action: clicking the Order Now button.
Start by updating the form’s onSubmit handler. Use Axios to make a POST
request to the API that includes the order items and the user details.
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = (event) => {
event.preventDefault();
// TODO
axios.post('/api/orders', {
items: cart,
name,
phone,
zipCode,
});
};
...
items, name, and zipCode are required fields. Ensure that the POST body
includes these fields, as shown here.
Save your file. In your browser, open the DevTools to the Network tab. Add
an item to your cart and navigate to the cart page. Enter user information in
the checkout form and click the Order Now button.
(Do not use the ZIP code 99999 when checking out. It intentionally causes an
error, which you will handle later in this chapter.)
You should see a network request to orders in your DevTools. Click it and
check out the headers pane to see information about the request, including the
201 status code that indicates the order was created (Figure 15.2, “Viewing
the created order in the Network tab”).
The server does not use persistent storage, so each time you restart your
server, you lose all the orders. Restart the server only if your computer
restarts or if you are taking a long break; otherwise, leave it running.
async/await
Now you have successfully submitted an order to the API. The next step is to
provide a nice user experience by letting the user know that their order was
created (or that there was an error).
Recall that when you were fetching items from the server, you used the
promise constructs then and catch, which let you know when the server
returned the items, so you could save them into state.
You face a similar issue here: Your app needs to wait until the network
request completes before it can notify the user about whether their order
submission was successful.
You will use promises again. This time, instead of then and catch, you will
use the async/await syntax to get the result. This syntax allows you to write
code that looks synchronous while still handling asynchronous behavior.
Label your submitOrder function as async, then await the Axios request.
After the wait, add a console.log statement indicating that the order was
submitted. (You will replace this with a user-facing message in the next
chapter.)
...
function Cart({ cart, dispatch, items }) {
...
const isFormValid = zipCode.length === 5 && name.trim();
Save your file. Back in your browser, switch your DevTools to the Console
tab, then click the Order Now button again. Your new success message prints
to your console (Figure 15.4, “Success message in the console”).
Recall that making a request with Axios automatically returns a promise. The
await keyword signals JavaScript to wait for the promise to complete before
executing any subsequent code in the function.
Frequently, you will see code that saves the data resulting from the promise
into a variable, like this: const result = await axios.post(...);.
However, when you submit an order, the server returns only a 201 status
code, indicating that the request was created. In this case, there is no resulting
data to save. You know the order is successful if your code makes it to the
next line, the call to console.log.
How do you know if the promise with await failed? The code throws an
error. You can catch this error using a try/catch block. Add this to your code
so you can log an error message when needed. (You will need to reindent the
lines you embed in the try block.)
Example 15.3. Adding an error message (Cart.js)
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = async (event) => {
event.preventDefault();
try {
await axios.post('/api/orders', {
...
});
console.log('Order Submitted');
} catch (error) {
console.error('Error submitting the order', error);
}
};
Save your file and return to your browser. Change the ZIP code to 99999 and
click Order Now. Because the API is written so that it returns a 400 status
error for this ZIP code, the error message prints to the console (Figure 15.5,
“Error message in the console”).
Open the Network tab to see the failed orders request highlighted in red.
Click the failed request, then open the response pane to see the error message
from the server (Figure 15.6, “Error message in the Network tab”).
Let’s look at a short demo on async/await and compare it with the promise
demo you saw in Chapter 8, Interacting with a Server. This demo file is also
in your downloaded resources file, at code-cafe-
resources/demos/asyncAwait.mjs. Though it is similar to the
promiseDemo.js demo file that you looked at before, instead of using then
and catch, this demo uses await inside a try/catch block.
Note: The .mjs extension allows you to use await at the top level of a file. In
your React application, you do not need this extension because you will use
functions with the async keyword instead.
'use strict';
console.log("top");
try {
const result = await getData();
console.log("result", result);
} catch (error) {
console.error("error", error);
}
console.log("end");
There are several differences between this file and promiseDemo.js. First,
instead of using getData().then, this code uses await getData(). Second,
a try block wraps the await. And third, the error-handling code uses a catch
block.
% node promiseDemo.js
top
getData Running
end
result yay!
In what order will the logs in asyncAwait.mjs print? Will it be the same
order as in promiseDemo.js, or will the order change?
cd YOUR_PATH/resources/code-cafe-resources/demos
node asyncAwait.mjs
% node asyncAwait.mjs
top
getData Running
result yay!
end
The first thing that logs is top. As before, this is because the code has not yet
invoked the getData function.
Next, the code invokes getData, which causes getData Running to log. As
before, JavaScript does not wait for the await result to start the promise; the
promise starts immediately when you invoke it.
Why does result yay! log before end now? Unlike then, the await
keyword pauses execution of the script and waits for the result to come back
before continuing to evaluate code. Frequently, this behavior is more aligned
with what developers expect.
On the other hand, there is a downside to pausing execution when you need
to execute several asynchronous things at once: await can cause them to run
sequentially instead of in parallel. To avoid this, you can group several
promises with Promise.all and await that result.
Before closing your demo, consider one more question: If you replace return
Promise.resolve("yay!"); with return Promise.reject("error!");,
what will happen? Check your answer before moving on.
Preventing Inadvertent Submissions
The delay between when the user clicks Order Now and when the request completes is
an issue not only for you and your code base. It can be an issue for your users too: A
user clicking the Order Now button multiple times could end up inadvertently creating
extra orders – especially if their network connection is slow and the request takes a
while to complete.
Right now, every part of the app runs locally on your machine, so everything loads
quickly. The DevTools let you test what would happen over a slow connection, so you
can see what the user might experience.
In the Network tab’s menu bar, click No Throttling and select Slow 3G in the dropdown
(Figure 15.7, “Modeling a slow network”).
When you select Slow 3G, the Network tab shows an exclamation point to remind you
that you have altered your internet access (Figure 15.8, “Altered network status
indicator”).
To prevent the user from accidentally submitting duplicate orders, add a new state
value called isSubmitting that is true while the request is completing. During this
state, disable the Order Now button to prevent additional orders.
...
function Cart({ cart, dispatch, items }) {
...
const [isEmployeeOfTheMonth, setIsEmployeeOfTheMonth] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const debounceRef = useRef(null);
...
const submitOrder = async (event) => {
event.preventDefault();
setIsSubmitting(true);
try {
...
} catch (error) {
console.error('Error submitting the order', error);
} finally {
setIsSubmitting(false);
}
...
return (
...
<label htmlFor="zipcode">
...
</label>
<button type="submit" disabled={!isFormValid}>
<button type="submit" disabled={!isFormValid || isSubmitting}>
Order Now
...
When you first initiate the request, you set isSubmitting to true. JavaScript does not
execute the remaining code in the function until the code following the await keyword
has completed. Finally, once the request is complete (or there is an error), you set
isSubmitting back to false.
You use the || operator to specify that the Order Now button should be disabled if the
form is invalid or if isSubmitting is true.
Now the Order Now button is disabled while the order is submitting (Figure 15.9,
“Disabled Order Now button”).
Before moving on, restore your model network to its full speed. In the Network tab’s
menu bar, click Slow 3G and select No Throttling in the dropdown.
Conclusion
Now users can submit orders to the API after adding items to the cart. You
handle the asynchronous behavior in React using the async/await syntax,
and you have implemented a new state to prevent users from
unintentionally submitting extra orders.
In the next chapter, you will add an alert to let users know whether their
order was successful or had an error.
Bronze Challenge: Reset the Cart State
After the user successfully submits an order, reset the state values for the
name, phone number, and ZIP code fields. If the order submission fails,
leave the values in place.
Silver Challenge: How Many Orders Are in the
Queue?
After submitting the order, users might like to know how many orders are
ahead of theirs.
The result will be an array of orders. Save the result into a variable, like in
the demo code above. The number of orders in the array will be the user’s
place in line. Log this value to the console.
Chapter 16. Component
Composition
In this chapter, you will use component composition to build a reusable alert
component. Component composition allows you to pass child components as
props to another component, giving you flexible control over what renders.
Component composition is a dynamic, powerful React tool that developers
frequently use in component libraries.
You will use the alert component to display messages in two scenarios:
When the order submission fails, you will display an error message.
By the end of this chapter, your cart page will be complete (Figure 16.1,
“Completed cart page”).
As we did for other components, we have provided the stylesheet for the
Alert component you are about to create. If you have not done so already,
copy Alert.css from your downloaded resources file into src/components.
Create a new Alert component that can display status messages. Remember
to import Alert.css. This component is different from previous components
you have built, because it relies on the children prop.
Example 16.1. Building the Alert component (Alert.js)
function Alert({
children,
}) {
return (
<div
className="alert-component visible"
>
{children}
</div>
);
}
Alert.propTypes = {
children: PropTypes.node.isRequired,
};
To see how this works, add the Alert component to Cart.js. Put it above the
cart header so it is nice and visible to the user. For now, set it to display a
thank-you message.
...
import './Cart.css';
import Alert from './Alert';
Now the alert displays at the top of the cart page, below the application
header (Figure 16.2, “Alert displayed on the cart page”).
You will add some controls in a moment so that the message appears only
when the user has submitted an order.
But first, open the Elements tab in the DevTools to inspect the elements.
Alert’s child string renders as a child of the nested <div> (Figure 16.3,
“Element hierarchy in the DevTools”).
You currently apply styles from two CSS classes to the component: alert-component
and visible. (Remember that in JSX, you apply styles from CSS classes using the
keyword className.) By default, the alert-component class hides the component from
view using CSS style rules. The visible class overrides those style rules to display the
component onscreen.
The visible class should apply only when the component should display. Add a
visible prop to the component. Then conditionally add the visible class name if the
prop value is true.
...
function Alert({
children,
visible,
}) {
return (
<div
className="alert-component visible"
className={`alert-component ${visible && 'visible'}`}
role="alert"
hidden={!visible}
>
...
Alert.propTypes = {
children: PropTypes.node.isRequired,
visible: PropTypes.bool.isRequired,
};
...
Now, when visible is true, both the alert-component and visible classes apply, so
the component displays to the user. When visible is false, only the alert-component
class applies, and the alert component does not display.
You also added the role of "alert" to the component. This role alerts accessibility
tools when the component changes and is visible onscreen. For example, screen readers
will read the contents of the component when it becomes visible. Adding the attribute
hidden also ensures that screen readers will skip the component when it is not visible.
In the Cart component, add state for controlling when the success Alert is visible. The
initial state value will be false, and it will update to true only when the POST request
is successful.
...
function Cart({ cart, dispatch, items }) {
...
const [isEmployeeOfTheMonth, setIsEmployeeOfTheMonth] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const debounceRef = useRef(null);
...
const submitOrder = async (event) => {
event.preventDefault();
setIsSubmitting(true);
try {
...
});
console.log('Order Submitted');
setShowSuccessAlert(true);
} catch (error) {
...
return (
<div className="cart-component">
<Alert>Thank you for your order.</Alert>
<Alert visible={showSuccessAlert}>
Thank you for your order.
</Alert>
<h2>Your Cart</h2>
....
Save your file and try your new Alert in the browser. When you click Order Now, the
new alert appears at the top of the page (Figure 16.4, “Success alert in the browser”).
And if you wanted to use JSX with children, you would need to use curly braces like this:
But React recommends that you always pass child elements by nesting them within the
component’s opening and closing tags, so there is a React ESLint rule against passing them inline
using the children prop.
However, the children prop is just one case. There is another case in which you compose
components directly as props, and you are already using it.
Why does Route use element instead of just making Home a child element? Route is already using
child elements to specify child routes. Look at the nested routes for /details:
The :id and index routes are children of the /details route, and element specifies what to display
on each route. Other front-end frameworks such as Vue and Angular have a concept called slots
that lets you pass multiple sets of child elements and specify different purposes for them. Although
React does not have this concept, it does allow you to pass JSX as a prop, which accomplishes a
similar task.
When composing components, reach for actual children first, to keep your code easy to read. Then,
if you need multiple sets of child components, add a prop.
Reusing Alert
With only a small adjustment, you can reuse the Alert component to let the
user know when an error occurs.
Add a type prop to the Alert component and use it to add an inline
backgroundColor style.
const BACKGROUND_COLORS = {
success: '#adc6a8',
error: '#f5c6cb',
};
function Alert({
children,
visible,
type,
}) {
return (
<div
className={`alert-component ${visible && 'visible'}`}
role="alert"
hidden={!visible}
style={{ backgroundColor: BACKGROUND_COLORS[type] }}
>
...
Alert.propTypes = {
children: PropTypes.node.isRequired,
visible: PropTypes.bool.isRequired,
type: PropTypes.oneOf(['success', 'error']).isRequired,
};
...
...
function Cart({ cart, dispatch, items }) {
...
return (
<div className="cart-component">
<Alert visible={showSuccessAlert}>
<Alert visible={showSuccessAlert} type="success">
Thank you for your order.
...
Now create a new error alert. Similar to the success alert, this alert should be
visible only after an error has occurred with the order submission.
This time, instead of using a Boolean to set the visibility state, use the error
message that the server returns. (Recall that in the last chapter, you looked at
an error message in the Network tab of your DevTools.) This way, you can
provide the user with the information they need to correct the error instead of
showing a generic error message.
...
function Cart({ cart, dispatch, items }) {
...
const [isSubmitting, setIsSubmitting] = useState(false);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [apiError, setApiError] = useState('');
const debounceRef = useRef(null);
...
const submitOrder = async (event) => {
...
} catch (error) {
console.error('Error submitting the order', error);
setApiError(error?.response?.data?.error || 'Unknown Error');
} finally {
...
return (
<div className="cart-component">
<Alert visible={showSuccessAlert} type="success">
Thank you for your order.
</Alert>
<Alert visible={!!apiError} type="error">
<p>There was an error submitting your order.</p>
<p>{apiError}</p>
<p>Please try again.</p>
</Alert>
<h2>Your Cart</h2>
...
When setting the API error, you use optional chaining to avoid problems with
getting the error message from the server. If there is no error message, you use
the generic message “Unknown Error” instead.
Recall that the !! operator converts any falsy or truthy value to its respective
boolean. An empty string is false; any other string value is true. (Also,
remember that you can check the result of using operators like this in the
console.)
Save your file, return to your browser, and try to place an order with the ZIP
code 99999. Now the error alert appears at the top of the screen (Figure 16.5,
“Error alert”).
There is one problem. Once the error is visible onscreen, it does not go away –
even if you subsequently make a successful request. (Try it for yourself.)
Add logic to reset apiError to an empty string when the user initiates a new
request.
...
function Cart({ cart, dispatch, items }) {
...
const submitOrder = async (event) => {
event.preventDefault();
setIsSubmitting(true);
setApiError('');
try {
...
Save your file, submit an order with the 99999 ZIP code, and then resubmit
using any other ZIP code. The success alert replaces the error alert.
Emptying the Cart
One final consideration: It would be nice to empty the cart after a successful
order submission, so that the user can start their next order afresh.
Start by creating a new action in cartReducer called EMPTY to empty the cart.
...
import './Cart.css';
import Alert from './Alert';
import { CartTypes } from '../reducers/cartReducer';
Save your files and test the new functionality in your browser. After you
successfully submit an order, your cart is empty (Figure 16.6, “Empty cart
after submitting order”).
In the next chapter, you will learn about another hook called useContext as
you add functionality for users to log in to Code Café.
Gold Challenge: Closeable Alert
It would be nice if users could close the error alert once they have read the
message. Currently, they have to submit a successful order to make it go
away. Because you might eventually want multiple closeable alerts, add this
functionality in a new component.
CloseableAlert should
In Cart, the onClose function should set apiError back to an empty string.
This will make the alert no longer visible.
Chapter 17. Context
In this chapter, you will explore React context. Context provides a way for
components to access information stored higher in the component tree
without needing to pass props.
To see how context works, you will set up the logic to allow Code Café’s
users to log in. You will store user information at a high level in the app,
since multiple components will need to access it. And you will use context to
access the user information in your components.
The server API for Code Café already has login functionality, so all you need
to do is build the UI to enable it. You will build a new route, /login, that
displays a form where the user will enter a username and password.
You will also add a new section to the header, next to the cart icon, that
shows either the username and a button to log out or, if no user is logged in, a
link to the login page (Figure 17.1, “Code Café at the end of this chapter”).
As you work through this chapter, try to write the code without looking at the
solution first, then compare what you have written with the solution
provided.
Getting the Logged-In User
Although the user data is stored on the server, it needs to be available if the
user refreshes the page while visiting Code Café. The first step is to add state
for holding information about the currently logged-in user, if there is one.
Because you are not using currentUser yet, disable the lint rule about
unused variables with
// eslint-disable-next-line no-unused-vars
...
function App() {
const [items, setItems] = useState([]);
// eslint-disable-next-line no-unused-vars
const [currentUser, setCurrentUser] = useState({});
const [cart, dispatch] = useReducer(
...
useEffect(() => {
axios.get('/api/items')
.then((result) => setItems(result.data))
.catch(console.error);
}, []);
useEffect(() => {
axios.get('/api/auth/current-user')
.then((result) => setCurrentUser(result.data))
.catch(console.error);
}, []);
return (
...
Although you could write this request using async/await syntax, it would
require some extra work. The main function for the useEffect hook has to be
synchronous, not asynchronous. So you would need to refactor the request
into its own function and then call it from within the hook.
That is why we prefer to use promises with then and catch for simple data
fetching. But this is a style choice; in your own code, you might prefer
async/await instead.
You might be wondering why you do not just add to your existing useEffect
that fetches items from the server. Though you can, it would be harder to
adjust later if the dependencies for one API call change or if you need to
move one of the API calls to another component.
Hooks are designed to help separate concerns so that you can contain
unrelated logic in separate methods. Therefore, it is best to put each unrelated
API call in its own useEffect block.
Save your file and, in the browser, make sure the Network tab is open in the
DevTools. Click one of the current-user requests and check out the
response pane. It shows that the initial state for currentUser is an empty
object: {} (Figure 17.2, “Current user request”).
function UserDetails() {
const currentUser = {};
return (
<div className="user-details-component">
{ currentUser.username
? (
<div>
<img src={Profile} alt="profile" />
<p>{currentUser.username}</p>
</div>
) : <Link to="/login">Log In</Link> }
</div>
);
}
You use a ternary to conditionally render the profile icon and username if a user is
logged in, or the login link if not.
For now, you stub out currentUser as an empty object. You will update this value
soon.
...
import CartIcon from '../images/cart.svg';
import UserDetails from './UserDetails';
import './Header.css';
Save your files and take a look at your new and improved header in the browser
(Figure 17.3, “Login link in header”).
(The login link currently leads to your Page Not Found route. You will fix this when
you build the login page later in this chapter.)
You could pass currentUser as a prop from App to Header, and then from
Header to UserDetails. React developers have a name for this kind of
pattern, where you pass a prop through components that do not need it to
get to a component that does: prop drilling.
<Header cart={cart}>
<UserDetails currentUser={currentUser} />
</Header>
Component composition is a great tool when you only need to skip one
level and share something in a few places. But as Code Café expands, many
components at various levels will need currentUser. It will be better if App
can share it one time for all of its descendants.
Think of context like a river. You provide the context value upstream, and it
flows downstream, so any component below the context can access it if
needed. Context avoids prop drilling by allowing components to subscribe
to the context only if they need it.
Passing currentUser with Context
Create a new directory called src/contexts and a new file in it called CurrentUserContext.js.
The React method createContext returns a context object that contains a Provider and a
Consumer. Continuing the river metaphor, the Provider is the component that pours the context
value into the river to make it available downstream, and the Consumer is the component that
accesses that value.
Although it is possible to pass a default value to createContext, you usually need that only for
testing or if the Consumer is outside of the Provider. In Code Café, currentUser is stored in
App, so that is where the Provider component will be.
Add the context provider to App by embedding the <Header> and <Routes> elements in a new
<CurrentUserContext.Provider> element, as shown below. (You will need to reindent the
newly nested code. You can add the provider’s opening and closing tags and use ESLint’s auto-
fix to indent the other lines.)
...
import NotFound from './components/NotFound';
import { cartReducer, CartTypes, initialCartState } from './reducers/cartReducer';
import CurrentUserContext from './contexts/CurrentUserContext';
function App() {
const [items, setItems] = useState([]);
// eslint-disable-next-line no-unused-vars
const [currentUser, setCurrentUser] = useState({});
...
return (
<Router>
<CurrentUserContext.Provider
value={currentUser}
>
<Header cart={cart} />
...
</Routes>
)}
</CurrentUserContext.Provider>
</Router>
...
Why did you not place the provider around <Router>? The Router component is actually also a
context provider, which is how the route parameters make it into your components. Though it
would have worked to place <Router> below your provider, by convention, you place <Router>
as high as possible.
Save your files and head over to the browser. Open the DevTools Components tab. There is a
new component called Context.Provider. (You might need to refresh your browser to see it.)
Click the Context.Provider component to see information about it below the component tree
(Figure 17.4, “Viewing the context in the DevTools”).
In most cases, React automatically infers the name of the component from the name of the
function defining the component. In this case, that was not possible because you used
createContext to create the component. But React also provides a way to specify the
component name: You can edit the displayName property, like how you can edit the propTypes
property to specify prop types.
CurrentUserContext.displayName = 'CurrentUserContext';
Save your file and check the DevTools again to see your new display name (Figure 17.5,
“Context display name in the DevTools”).
...
function App() {
...
useEffect(() => {
axios.get('/api/auth/current-user')
.then((result) => setCurrentUser(result.data))
.catch(console.error);
}, []);
return (
<Router>
<CurrentUserContext.Provider
value={currentUser}
value={currentUserContextValue}
>
...
The ESLint warning gives a clue as to how to fix the problem: Use the
useMemo hook to create a memoized version of this object. A memo is like a
cache, holding on to a value until it needs to change. In Chapter 19,
Introduction to App Performance Optimization, you will learn about using
memos for values that are expensive to calculate. In this chapter, you will use
a memo to hold on to an object so its identity does not change unnecessarily.
a callback function
a dependency array
useMemo executes the callback function and returns a memoized value. React
uses this value until the callback function executes again. During each render
cycle, React compares the contents of the dependency array and reruns the
callback function if there are changes.
return (
...
(At least, this is generally the case. Although React uses memos to improve
efficiency, it does not guarantee that it will execute the callback function only
when the values change. It is possible for React to clean out some memory or
perform another operation that requires rerunning the function.)
You will learn more about useMemo in Chapter 19, Introduction to App
Performance Optimization.
Check the DevTools to see the updated values of the context (Figure 17.7,
“Context values”).
function UserDetails() {
const currentUser = {};
const { currentUser } = useContext(CurrentUserContext);
return (
...
The useContext hook takes one argument: the context object you are
referencing. The hook returns the value from the nearest matching context
provider. You use object destructuring to target the specific context values the
component needs.
Currently, UserDetails does not update the user information, so it does not
need access to the setter function. (That will change later in this chapter.)
Now, UserDetails has access to currentUser. Its parent, Header, does not.
This is just what you want.
Save your file and check the DevTools Components tab. Select the
UserDetails component in the tree, then expand the hooks section in the
bottom pane. You can see the value of currentUser, showing that the
component does indeed have access to the context object (Figure 17.8,
“Context values in the DevTools”).
Figure 17.8. Context values in the DevTools
Login Component
Now you are ready to build the Login component in a new components/Login.js file. Copy the
Login.css stylesheet from your downloaded resources file into your components directory, if
you have not already done so, and import it in the component. Then create a form that requests a
username and password and stores the user’s input in state. Do not worry about handling the
form’s onSubmit event yet.
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
return (
<div className="login-component">
<h2>Log In</h2>
<form>
<div>
<label htmlFor="username">
Username
<input
type="text"
id="username"
value={username}
autoComplete="username"
onChange={(event) => setUsername(event.target.value)}
required
/>
</label>
</div>
<div>
<label htmlFor="password">
Password
<input
type="password"
id="password"
value={password}
autoComplete="current-password"
onChange={(event) => setPassword(event.target.value)}
required
/>
</label>
</div>
<button type="submit">Log In</button>
</form>
</div>
);
}
Next, add the new component as a new route on App for the /login path.
...
import { cartReducer, CartTypes, initialCartState } from './reducers/cartReducer';
import CurrentUserContext from './contexts/CurrentUserContext';
import Login from './components/Login';
function App() {
...
return (
...
</Route>
<Route path="/" element={<Home items={items} />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
...
Save your files. In the browser, navigate to the new route by clicking the Log In link in the
header (Figure 17.9, “New login page”).
When the user submits the form, make a POST request to /api/auth/login with the data {
username, password }.
On success, call setCurrentUser, which you can get from context, and pass it result.data.
(This is similar to the request for /api/auth/current-user.) In the error case, log to the
console. Later, you will replace this behavior using the Alert component.
function Login() {
const { setCurrentUser } = useContext(CurrentUserContext);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
return (
<div className="login-component">
<h2>Log In</h2>
<form>
<form onSubmit={login}>
<div>
...
Though the API accepts any username, the only valid password is pass.
First, try logging in with any username and an invalid password (such as invalid). Open the
console in the DevTools to see the error (Figure 17.10, “Login error”).
Because the server uses a cookie for the login, you will stay logged in even if you refresh the
page.
useNavigate
So far, so good. But you are still on the login page. After the user
successfully logs in, it would be nice to redirect them to the home page.
Previously, you used the Link component from React Router to change the
user’s location when they clicked the Link. React Router also provides the
useNavigate hook to perform a similar change programmatically.
This hook returns a function that takes in the new location, using the same
format as the to attribute of Link. It has options that dictate how to add the
new location to the history. By default, it pushes a new browser state, which
means the user can click the back button in the browser to go back to the
previous page.
Complete the login form and click Log In again. This time, the app redirects
you to the home page.
Logout Button
For better security, add a logout button to the header when the user is logged in.
return (
...
<img src={Profile} alt="profile" />
<p>{currentUser.username}</p>
<button type="button" onClick={logout}>
Log Out
</button>
</div>
...
The server returns the error in the same place as it does when you submit an
order. You can access it from the Axios error object with
error?.response?.data?.error.
function Login() {
const { setCurrentUser } = useContext(CurrentUserContext);
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [apiError, setApiError] = useState('');
return (
<div className="login-component">
<Alert visible={!!apiError} type="error">
<p>There was an error logging in.</p>
<p>{ apiError }</p>
<p>Please try again.</p>
</Alert>
<h2>Log In</h2>
...
Save your file and try to log in to Code Café with an invalid password.
You will get an error message (Figure 17.13, “Login error message”):
Now users can log in and log out, and Code Café is nearly feature
complete! In the next chapter, you will build a page to allow the café’s
associates to view and complete submitted orders.
For the More Curious: Login Cookie
How does the server remember that you logged in? It uses a cookie.
You can find the login cookie in the DevTools Application tab.
If you want to learn more about JSON Web Tokens, their website has a nice
introduction section: jwt.io/introduction.
Silver Challenge: Items Context
Convert items to a context value so you no longer have to pass it as a prop
to the components.
Note: Since the components do not change items, you do not need to put
setItems in context.
Because this challenge will affect the tests you write later in the book, be
sure to work on it in a copy of your code.
Chapter 18. Fulfilling Orders
In the last chapter, you set up React context so that when a user logs in, you can easily
share their information with components at multiple levels throughout the app.
Now that users can log in, you will build on concepts you have already learned to
create an orders page that displays existing orders to users with associate access
(Figure 18.1, “Completed orders page”).
import './Orders.css';
function Orders() {
return (
<div className="orders-component">
<h2>Existing Orders</h2>
</div>
);
}
Now add the new component as a new route on App for the /orders path. Pass the
items array as a prop so you can use it in the Orders component.
...
import CurrentUserContext from './contexts/CurrentUserContext';
import Login from './components/Login';
import Orders from './components/Orders';
function App() {
...
return (
...
<Route path="/" element={<Home items={items} />} />
<Route path="/login" element={<Login />} />
<Route path="/orders" element={<Orders items={items} />} />
<Route path="*" element={<NotFound />} />
...
Save your files and visit http://localhost:3000/orders to see the new page (Figure 18.2,
“Existing orders”).
Only associates should see this link. Logged-out users or users without associate-level
access should not.
When a user logs in, the server returns a user details object that contains a property called
access. For associates, this value is "associate". For other users, it is an empty string.
Use a ternary to conditionally render the orders link in the UserDetails component.
...
function UserDetails() {
...
return (
<div className="user-details-component">
{ currentUser.username
? (
<div>
{currentUser.access === 'associate'
? <Link to="/orders">Orders</Link>
: null}
<img src={Profile} alt="profile" />
...
Your ternary returns null when the user is not an associate. React skips rendering when
the ternary resolves to false, null, or undefined, so users who are not associates will not
see the link.
You could also have used && to write this check with less code:
When a statement like this resolves to a Boolean false, React does not render anything.
In this case, the statement either resolves to false (when the user is not an associate) or
returns a link (when they are).
Save your file. In the browser, log in with the username Guest and the password pass to
test what happens when a user without associate access logs in. There are no visual
changes in the header – the orders link does not render (Figure 18.3, “No orders link for a
logged-in guest”).
Figure 18.3. No orders link for a logged-in guest
Log out. Code Café’s server is configured to treat any username other than Guest as an
associate. Log in again with any other username and the password pass. The orders link
appears (Figure 18.4, “Orders link for a logged-in associate”):
Click the link to confirm that it takes you to the orders page.
Fetching and Displaying Orders
Now you need to fetch the orders from the API.
A GET request to /api/orders returns an array of orders, where each order object has the
following shape:
{
id: string,
name: string,
zipCode: string,
phone: string, // optional
items: [{
itemId: string,
quantity: number
}]
}
Recall that the phone number field in the order form is optional. The items array contains item
objects with an itemId and a quantity, following the same structure as the cart items.
You passed items to Orders as a prop. You will use the itemId to find each order item in the
items array so you can display the necessary information.
Add the GET request to Orders and store the orders in state. Add the items prop to the
component and define it using prop types.
Because you are not using items or orders yet, you will need to disable the ESLint rule about
unused variables (or ignore the lint error). You can disable the rule with // eslint-disable-
next-line no-unused-vars.
// eslint-disable-next-line no-unused-vars
function Orders() {
function Orders({ items }) {
// eslint-disable-next-line no-unused-vars
const [orders, setOrders] = useState([]);
useEffect(
() => {
axios.get('/api/orders')
.then((result) => setOrders(result.data))
.catch(console.error);
},
[],
);
return (
<div className="orders-component">
<h2>Existing Orders</h2>
</div>
);
}
Orders.propTypes = {
items: PropTypes.arrayOf(ItemType).isRequired,
};
Save your file. In the browser, navigate to the orders page. Open the DevTools Components tab
and look at the hooks section to see the initial state for Orders. Then add an item to the cart,
place the order, and return to the orders page. Check the DevTools again: Now the state is
populated with the order information (Figure 18.5, “Order displaying in the DevTools”).
...
import ItemType from '../types/item';
import './Orders.css';
// eslint-disable-next-line no-unused-vars
function Orders({ items }) {
// eslint-disable-next-line no-unused-vars
const [orders, setOrders] = useState([]);
useEffect(
...
);
return (
<div className="orders-component">
<h2>Existing Orders</h2>
{orders.length === 0
? <div>No Orders</div>
: orders.map((order) => (
<div className="order" key={order.id}>
<table>
<thead>
<tr>
<th>Customer</th>
<th>ZIP Code</th>
{order.phone && <th>Phone</th>}
</tr>
</thead>
<tbody>
<tr>
<td>{order.name}</td>
<td>{order.zipCode}</td>
{order.phone && <td>{order.phone}</td>}
</tr>
</tbody>
<thead>
<tr>
<th>Quantity</th>
<th>Item</th>
</tr>
</thead>
<tbody>
{order.items.map((item) => (
<tr key={item.itemId}>
<td>{item.quantity}</td>
<td>{items.find((i) => i.itemId === item.itemId)?.title}</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
...
You use a table to display the orders so that the columns line up correctly, just like you did with
the cart. You conditionally render the phone number, because the field might not exist on the
order object. When finding the item with the array find method, you use the optional chaining
operator (?.) to display the title only if the item is found.
Save your file, return to the browser, and place a couple of orders in Code Café. Make sure you
are logged in as an associate, then visit the orders page to view the orders (Figure 18.6,
“Viewing order details”).
axios.delete makes a call with the DELETE method. You can use a
template literal to dynamically create the URL with the order ID at the end.
If the request is successful, the Order component should load the orders
again. If not, log the error to the console.
...
function Orders({ items }) {
const [orders, setOrders] = useState([]);
useEffect(
() => {
axios.get('/api/orders')
.then((result) => setOrders(result.data))
.catch(console.error);
loadOrders();
},
[],
);
return (
...
<tbody>
{order.items.map((item) => (
...
))}
</tbody>
</table>
<button
type="button"
onClick={() => deleteOrder(order)}
>
Delete Order
</button>
</div>
...
Save your file and visit the orders page in your browser to see the new button
(Figure 18.7, “Delete Order button”).
The orders remain visible on the page (Figure 18.9, “Orders are always visible”).
Now try to delete an order. Nothing happens. However, if you check the Console or
Network tabs in the DevTools, you will see that the server rejected the request with
the code 401 (unauthorized) (Figure 18.10, “Unauthorized order deletion”).
Custom hooks
Users should not be poking around in data that is meant for café associates, even if
they cannot delete that data. You will limit access to the orders page based on the
user’s access level.
Custom hooks are JavaScript functions that utilize other hooks. You define the
custom hook’s arguments and return value as you would for other JavaScript
functions. Like other hooks, custom hooks must have a name that begins with use.
CurrentUserContext.displayName = 'CurrentUserContext';
export const useCurrentUserContext = () => {
const context = useContext(CurrentUserContext);
return context;
};
Import and use your new hook in Orders to get the value of currentUser. (Until
you use the value, you will have to disable the ESLint rule about unused variables.)
...
import ItemType from '../types/item';
import './Orders.css';
import { useCurrentUserContext } from '../contexts/CurrentUserContext';
Instead of importing CurrentUserContext and the useContext hook, you have one
straightforward import that takes no arguments and returns the value you need. You
can also use this custom hook in the Login and UserDetails components. In fact, it
will make your code cleaner, so a challenge at the end of this chapter asks you to
make this change.
Save your file. In your browser, check the DevTools again to ensure that you are
getting the correct values for the current user’s username and access level
(Figure 18.11, “Context in the DevTools”).
Now that Orders knows the currentUser, you can ensure that only associates have
access to the orders.
Additionally, add a message letting unauthorized users know that they cannot
access the orders.
...
function Orders({ items }) {
const [orders, setOrders] = useState([]);
// eslint-disable-next-line no-unused-vars
const { currentUser } = useCurrentUserContext();
...
useEffect(
() => {
if (currentUser.access === 'associate') {
loadOrders();
}
},
[],
[currentUser],
);
...
return (
<div className="orders-component">
<h2>Existing Orders</h2>
{orders.length === 0
? <div>No Orders</div>
? (
<div>
{currentUser.access === 'associate'
? 'No Orders'
: 'Access Denied'}
</div>
)
: orders.map((order) => (
...
Your existing ternary returns the string "No Orders" when orders.length is 0.
Here, you add a nested ternary to the code that executes when the first ternary’s
predicate is truthy. In it, you return the string "Access Denied" instead of "No
Orders" when the current user does not have associate access. This provides more
specific feedback to the user.
Save your file and try the new functionality in your browser: Log in as an associate
and visit the orders page. You see the list of orders as expected. Now log out, using
the button in the header. The orders are still visible.
Refresh the page to see the “Access Denied” message (Figure 18.12, “Access
Denied”).
However, you had to refresh the page before the “Access Denied” message
appeared. When you first logged out, the orders remained visible on the page.
How can you ensure that React updates to hide the orders when the user logs out?
Cleaning Up useEffect
React accepts a cleanup function as the return value of useEffect. If a
dependency for useEffect changes, React calls the cleanup function before
it runs the main effect again. React also calls the cleanup function when the
component is about to be destroyed.
...
function Orders({ items }) {
...
useEffect(
() => {
if (currentUser.access === 'associate') {
loadOrders();
return () => {
setOrders([]);
};
}
return () => { };
},
[currentUser],
);
...
React calls your new cleanup function when the user navigates to another
page and the Order component is destroyed. React will also call the cleanup
function when currentUser changes, because you added currentUser as a
dependency to useEffect earlier.
Below the if block, you add an empty function as the default return value.
This is because ESLint expects all branches of useEffect to have a
consistent return pattern. The conditional if is a branch. Because the if
branch includes a cleanup function, the default branch must also include a
cleanup function. There is nothing that needs to happen in this case, so you
leave the return function empty.
Save your file and try the experiment again in the browser: Log in as an
associate and navigate to the orders page. Now log out.
Ta-da! The “Access Denied” message appears right away. You do not need
to refresh the page.
Before we move on from the topic of security, we should mention that Code
Café is still not fully secure. Though the front end restricts access to the
orders page, the complete source code for your React application –
including the Orders component – is still publicly available to all users.
This means that hackers and malicious users can read through the code to
find the order deletion endpoint or hack the context values to make React
think they are associates.
One other reminder about security: Since the complete source code of your
application is available to all users, never store secret values in the React
application. Store those only on the server.
Getting New Orders
The orders page has a problem: It does not update with new orders.
Open the application in two browser tabs. View the orders page in one tab. In the
other tab, place an order. The tab with the orders page will not show the new order
until you refresh it.
Although you could solve this by polling the server at specified intervals,
websockets are a better tool. Websockets allow you to subscribe to updates, so the
browser does not have to poll and instead gets an update each time an update is
there.
Proxying a websocket
Recall that you set up a proxy for the API. It is also possible to proxy a websocket
connection. This is not built into the previous proxy configuration, so you will
need to manually configure the proxy.
First, you need a new package. In a terminal separate from your app, cd into your
code-cafe directory. Then run npm install --save http-proxy-
[email protected] to install the http-proxy-middleware package.
We selected this version because it is the one already bundled in the development
server.
Now you can create a custom proxy. Open package.json and delete the previous
proxy you set up.
...
"scripts": {
...
},
"proxy": "http://localhost:3030",
"eslintConfig": {
...
Now create a new file in the src directory called setupProxy.js. Add the proxy
configuration to this file.
Example 18.12. Setting up the new proxy (setupProxy.js)
If you previously needed to change the proxy to 127.0.0.1, you will need to do
that here as well. In setupProxy.js, replace
with
The development server inside Create React App uses the Express framework. You
use the Express function app.use to add a piece of middleware, which, in this
case, is a proxy from the http-proxy-middleware package.
The first parameter of createProxyMiddleware is the path it should use. The API
requests are at /api, and the websocket will be /ws-cafe. You use ['/api',
'/ws-cafe'] to match both.
changeOrigin, which changes the origin of the host header to the target URL
(this is necessary only for name-based virtually hosted sites; however, it is
easiest to include it everywhere)
You can read more about the options in the documentation for the package at
github.com/chimurai/http-proxy-middleware.
Once Create React App restarts, it automatically executes in src/setupProxy.js.
In the terminal tab currently running Code Café, stop the app with Control-C and
restart it using npm start.
The server already supports websocket connections, so you are ready to create a
websocket connection on the orders page.
...
function Orders({ items }) {
const [orders, setOrders] = useState([]);
const { currentUser } = useCurrentUserContext();
useEffect(
() => {
if (currentUser.access === 'associate') {
loadOrders();
const ws = new WebSocket(`${(
window.location.protocol === 'https:' ? 'wss://' : 'ws://'
)}${window.location.host}/ws-cafe`);
ws.onopen = () => {
console.log('connected');
};
ws.onerror = (event) => {
console.error(event);
};
ws.onmessage = (message) => {
const newOrders = JSON.parse(message.data);
setOrders(newOrders);
};
ws.onclose = () => {
console.log('disconnected');
};
return () => {
...
const deleteOrder = async (order) => {
try {
await axios.delete(`api/orders/${order.id}`);
loadOrders();
} catch (error) {
...
You replace the previous code to load orders with code that sets up a websocket
and opens a websocket connection.
Once the websocket exists, you tell it what to do for different events:
For most of these events, the code logs the event to the console to help with
debugging. The exception is onmessage.
The server sends a message with the list of orders, via the websocket. This
happens right after the websocket connects and also when orders are created,
updated, or deleted. The message sends as a JSON-encoded string, so you use
JSON.parse to get the message. Then you call setOrders with the updated orders
from the server.
The URL to connect to the websocket is a template literal. The first expression in
the URL determines the protocol for the websocket request. If the user is on
https:, the request uses wss:// for a secure websocket. Otherwise, it uses ws://.
Save your files and try the experiment again, using two browser tabs to watch for a
newly placed order. (Make sure you are logged in, so you can open the websocket
and view the orders.) The orders page immediately updates when you place a new
order.
...
function Orders({ items }) {
...
useEffect(
...
return () => {
ws.close();
setOrders([]);
...
As we said earlier, the cleanup function executes when the component will be
destroyed or when the useEffect hook will run again (such as when currentUser
changes). So adding the websocket-closing logic to useEffect’s cleanup function
ensures that the websocket closes when the user navigates away from the orders
page or logs out.
Try it out: Make sure you are logged in and viewing the orders page. In the
DevTools, open the Console tab. You will see some logs about a failed websocket
connection. These logs are the result of React’s rerunning the effect in
development mode.
The last log message should say connected. Click the button in the header to log
out. The orders disappear and, in the console, you see a new log message saying
that the websocket has disconnected (Figure 18.13, “Websocket disconnected”).
Now the Code Café application has all its features. In the next chapter, you
will learn about tools that React offers to track and optimize the
performance of your app. After that, you will use Code Café to learn about
testing.
Bronze Challenge: Smarter Redirect
Currently, the app sends all users to the home page after they log in. But
associates are usually checking the orders when they log in, not trying to
place an order. Edit the Login component so that if a user is an associate, it
redirects them to the orders page. For a guest user, it should still redirect
them to the home page.
This challenge interferes with the expectations for a test in Chapter 22, End-
to-End Testing, so make sure to work on it in a copy of your project.
Bronze Challenge: Import a Custom Hook
In this chapter, you made the custom hook useCurrentUserContext to get
the value of currentUser from context.
You use this context value in two more places. Replace the instances of the
useContext hook in Login and UserDetails with your custom hook.
Silver Challenge: Loader
Currently, the user sees “No Orders” while the orders are loading, even if
there are actually orders that will load.
Add a new state variable to track loading. It should initially be true. When
the websocket gets a message with the orders, the variable should update to
false.
If the server returns an empty array, the page should show “No Orders,” just
like it does now. Otherwise, it should show the orders.
Silver Challenge: Error Display
Most users will not be looking at the Network or Console tabs in the
DevTools.
Use the Alert component to show an error message if the request to delete
an order fails.
Chapter 19. Introduction to App
Performance Optimization
Look how far you have come with Code Café! Your app lets users view and
select items for sale, review items in the cart, enter information in a form, and
send orders to the café. It also allows associates – but not other users – to
review the orders that customers have placed.
As you have developed Code Café, your knowledge of and experience with
React have grown. You have built components, added multiple pages with
routing, passed data using props and context, and implemented several React
hooks. Now you will turn your attention to how your app performs.
In the final two chapters of this book, you will investigate several issues that
affect app performance using an app we will provide for you.
In this chapter, you will learn more about the React render cycle using your
Code Café code base.
React re-renders an app when its state changes to keep the UI in sync with the
data. When you build a component, you describe how you want the app to
look in certain scenarios. For example, “If the user is logged in, display the
user details. If not, show the login button.” Each time the user state changes,
React re-renders and recalculates what it should present onscreen.
One reason React is so popular is that it is pretty fast and – most of the time –
you can leave it alone to do its job. But you might sometimes find that your
app runs slowly or that components seem to re-render more often than they
should.
In this chapter, you will use the React Profiler tool to track performance. You
will learn about tools that React provides to optimize your components,
including the React function memo, a new hook called useCallback, and the
useMemo hook you first saw in Chapter 17, Context.
There is a cost associated with adding any code to your application, so you
normally add optimizations only when an app has a performance issue. Code
Café is a small app, and it runs quickly in its current form. If it were a real-
world app, you would not add any optimizations to it. However, you will
need to use the tools covered in this chapter as you build larger applications,
and Code Café is a good project to practice in as you learn how to use them.
To begin, let’s take a look at the React Profiler tool and see what happens in
the app when a user adds an item to their cart.
React Profiler
In your browser, navigate to the details page for any item and open the React
DevTools to the Profiler tab. Using the Profiler, you can record React’s
render cycle and see how long each component takes to render.
Before you start, you will set the Profiler to record the reason each
component renders, in addition to the time it takes to render. Click the
settings icon in the Profiler tab (Figure 19.1, “Opening the Profiler settings”):
You can view the results of the recording as either a flamegraph or a bar
chart. Select the flamegraph chart, which has an icon that looks like a flame
(Figure 19.4, “Viewing the flamegraph”).
Toward the bottom of the graph, you can see that the Details component and
its children (the Thumbnails in the sidebar) re-rendered when you clicked Add
to Cart.
Click the bar for the Details component to see more information
(Figure 19.5, “Details flamegraph”).
Figure 19.5. Details flamegraph
The Profiler tool reports that Details re-rendered because its parent re-
rendered. Recall that functional components always render when their parent
renders. The parent of Details is App, so this tells you that App must have re-
rendered. Why?
Click App to find out. The Profiler tool reports that Hook 3 changed. Switch
to the Components tab in the DevTools and select App. In the hooks section,
you can see that hook 3 is the cart reducer.
Adding an item to the cart executes the cart reducer, which updates the state
of the hook. This causes React to start a render cycle with the component that
contains that hook: App.
Switch back to the Profiler tab and select Details again. In the right-side
pane, the Rendered at information supplies a timestamp indicating when the
component rendered during the profiling session and how long it took to
render. In this case, Details rendered in 2.7 milliseconds. (Again, your exact
result might not be the same as ours.)
Although that is not very long, when Details renders, its children – all the
Thumbnails under it – must also render. Because nothing has changed in the
Details UI, it would be nice if React did not re-render it.
Memo
Recall from Chapter 4, State that React supports class components as well as
the functional components you have been using. Unlike functional
components, class components re-render only if their props, state, or context
changes; they do not automatically re-render along with their parent
components.
When memo wraps a component, it creates a memo. (We say that it memoizes
the component.) A memo is how React remembers the references of the
component’s state, props, and context. During render cycles, React compares
the current references with the previous ones. If there are no changes, React
does not re-render the component.
This comparison is an extra job that React has to perform during the render
cycle, so use it only when there are performance concerns. In Code Café,
although the Details component re-renders often and with the same props, it
is fast, so it would not need a memo in a production app. If it were a heavy
component, taking a long time to render, it would be a good candidate for
memoization in a production app.
(memo is not the same as the useMemo hook you used when you set up
CurrentUserContext in Chapter 17, Context. memo wraps a component to
prevent unnecessary re-renders, while useMemo is a hook that prevents
unnecessary expensive recalculations.)
To see how memoization works, wrap the Details component in the memo
function.
You wrap the default export of Details with memo to memoize the result.
Now, when you import Details in App.js, the import statement will read the
default export and then import the memoized version of Details.
Save your file and refresh the page in your browser. Then run the experiment
again:
3. Stop recording.
The Profiler reports that the component rendered due to a context change and
a props change. First, let’s look at the context change.
DetailItem uses the useParams hook from React Router to find the id of the
item to display. Like the Link component, this hook causes the component to
re-render because React Router’s context has changed.
Try it out.
DetailItem.propTypes = {
const sharedProps = {
addToCart: PropTypes.func.isRequired,
items: PropTypes.arrayOf(ItemType).isRequired,
};
DetailItem.propTypes = {
...sharedProps,
id: PropTypes.string.isRequired,
};
DetailsOuter.propTypes = sharedProps;
Save your file and try the experiment again (Figure 19.9, “Wrapped
DetailItem”).
addToCart comes from App. When the user adds an item to the cart, the state of the cart
reducer changes, causing the App component to re-render. The addToCart function is
re-created each time App renders – which means the reference changes, even though the
contents of the function are the same.
To avoid unnecessary renders, React provides a hook called useCallback that prevents
functions from being re-created. useCallback accepts two arguments:
a dependency array with the items that should cause the callback to be re-created
In App, wrap addToCart in useCallback to prevent it from being re-created each time
App re-renders.
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(cart));
...
To prevent stale data, you typically include in the dependency array all the variables in
the callback, including other functions. The exceptions are values that React guarantees
to remain the same, including dispatch and the useState setter function. So although
addToCart uses the dispatch function, you do not include it in the dependency array,
because it will not change.
Since there are no items in the dependency array, addToCart will be created only on the
initial render of App and will not change in subsequent renders.
Save your file and use the Profiler to record adding an item to the cart one last time
(Figure 19.10, “DetailItem no longer re-renders”).
So does this mean you should wrap all your functions in useCallback? Definitely not.
There is a cost associated with verifying the dependencies and tracking the original
function. Use useCallback only when you need to avoid unnecessary re-renders.
Expensive Calculations
Now you have optimized Details and DetailItem to prevent unnecessary re-renders. But
what about expensive calculations that happen during legitimate renders?
In Code Café, add items to your cart and visit the cart page. Recall that in Cart.js, you use
taxPercentage and taxRate to calculate the tax rate based on the user’s ZIP code. React
performs these calculations each time the component re-renders.
Let’s see how often that happens. Add a console.log statement to check how often the tax
calculations run.
...
function Cart({ cart, dispatch, items }) {
...
const subTotal = isEmployeeOfTheMonth ? 0 : cart.reduce((acc, item) => {
...
}, 0);
console.log('compute tax');
const taxPercentage = parseInt(zipCode.substring(0, 1) || '0', 10) + 1;
...
Save your file. Back in the browser, begin recording in the React Profiler to track
component re-renders.
Input a name in the checkout form, then stop recording. Take a look at the results
(Figure 19.11, “Cart Profiler results”).
In this example, there were eight renders. This is because the name we used, Customer, has
eight characters. Each time a character was entered, React updated the UI to ensure that it
was in sync with the state.
(You will likely see a different count, depending on the name you entered and whether you
pressed Delete and retyped characters.)
Now switch to the console to see the output from the log statement you added. The number
of console statements should match the number of renders shown in the Profiler
(Figure 19.13, “Viewing the compute tax logs”).
(You likely have duplicate logs printed, as in the example above. This is due to the
React.StrictMode tool, which renders each component twice when you are running in
development mode. Focus on the logs that come from Cart.js.)
Although the re-renders of Cart are expected, these eight log statements indicate that React
is also running the tax calculations each cycle. This is not optimal, since the customer’s
name has nothing to do with the taxes charged.
Computing on every render is not problematic if the computations are very fast. But if they
are slow, they can slow down rendering – and slow down users trying to type into a form.
Although Code Café’s tax calculation is simple and fast, tax calculations in general are
complicated and can be slow. You will optimize the tax calculations to rerun only when the
ZIP code changes.
In Chapter 17, Context, you used the useMemo hook when setting the values for
CurrentUserContext. You will use it again here. useMemo returns a memoized value and
recomputes only when its dependency array changes.
Add useMemo to the tax calculations in Cart.js. (You will need to reindent some of the
existing code.)
Here, the function you pass to useMemo returns the tax rate, which useMemo caches. Because
you include zipCode in the dependency array, React recalculates the tax rate only when
zipCode changes.
Save your file. In the browser, make sure you are on the cart page. Clear the Console tab
with the ⊘ button near the left end of the menu bar, then type a name in the checkout
form.
Now add a ZIP code. compute tax logs each time the ZIP code changes.
Comparing useCallback, useMemo, and memo
The tools you have used in this chapter have similar names and purposes.
Keeping track of when to use each one can be confusing until you build up
some experience with them. Let’s recap what you have learned.
useCallback and useMemo are similar in that they rely on the dependency
array to perform their optimizations. However, they serve different purposes
and return different values: You use the useCallback hook to maintain
reference equality by preventing unnecessary re-creation of the function
that the hook returns. You use the useMemo hook to avoid expensive
calculations by preventing unnecessary execution of the function whose
result the hook returns.
...
function Cart({ cart, dispatch, items }) {
...
const taxRate = useMemo(
() => {
console.log('compute tax');
const taxPercentage = parseInt(zipCode.substring(0, 1) || '0', 10) + 1;
...
Conclusion
In this chapter, you saw how to optimize components using memo,
useCallback, and useMemo. You also took the React Profiler for a spin.
In the next three chapters, you will dive into testing, using various
techniques to test Code Café. Then, in the final two chapters of the book,
you will explore other issues that can affect app performance and see more
ways you can optimize your apps to give your users the best possible
experience.
Silver Challenge: Memoized UserDetails
Use the React Profiler to examine Header while you add an item to the cart.
Header re-renders, and so does UserDetails.
Use memo to prevent UserDetails from re-rendering when you add items to
the cart.
Note: As you saw with the Details component, the Link component inside
UserDetails will still re-render.
Chapter 20. Testing Overview
You will finish your work on Code Café by adding tests to check that things
are working properly.
This chapter introduces four types of testing and discusses the trade-offs
between them. When testing a React application, you will probably use
more than one kind of test at any given time – and different sets of tests at
different times.
You will not do any coding as you read this chapter. In the next two
chapters, you will implement two types of tests from this chapter: unit tests,
using Jest and the React Testing Library, and end-to-end tests, using
Cypress and the Cypress Testing Library.
Ideally, you are implementing testing to create dependable code from the
beginning of a project.
But we do not always live in an ideal world. While the beginning of the
project is the best time to introduce testing, the second best time is now!
One reason to introduce tests to a previously untested project might be that
you are about to refactor the project, and you want to be sure the application
works the same after refactoring as it did before.
With any of these motivations, your first instinct might be to write tests that
generate high code coverage (meaning they check lots of things) using
automated code inspections. However, high code coverage does not
guarantee that your tests will actually catch bugs.
What you want to know from your tests affects how many of them you
write and at what level of the application you write them. Some things you
might want to know are that
the basic happy paths (flows that should execute without throwing
exceptions) work
every happy path and every unhappy path (which should result in an
error) produces the expected outcome
Bear in mind that trying to test every possible path through an application is
rarely possible, due to path explosion – the number of available paths
increasing as the app grows more complex.
Imagine an application similar to Code Café but with two ways to add an
item to the cart. Once on the cart page, this app’s user can edit or not edit
the cart. Then, the user can check out with Visa or PayPal, and the
transaction can be approved or declined. This is already 16 possible paths (2
× 2 × 2 × 2, or 2⁴).
Sixteen paths might be testable. But these paths do not include the user
searching for items or needing to go back in the checkout flow – or lots of
other possible scenarios. It quickly becomes impossible to test everything,
even in a fairly simple app.
Having a good idea about what you want to gain from your tests can help
determine which paths to focus on.
Are you planning to start a refactor? Are you in a discovery phase, so you
need to be ready to pivot the app in a new direction? Or are you just adding
incremental functionality to your app, or maybe fixing bugs?
Jumping into testing without considering the future can lead to extra work,
such as needing to rewrite your tests because you refactored the feature they
were testing.
(This is not to say you will never refactor tests. There are cases where, even
though you know you will need to refactor the tests later, you implement
granular testing – detailed testing of very specific items. When granular
tests fail, they give you a very specific point of failure, which can be
valuable depending on your needs.)
Types of Testing
Each type of test has pros and cons, and there is no single best type.
Once you have an idea of why you are testing, what you want to know, and
what the future looks like, you can weigh the different types of testing to
find the best fit for your needs at the time. You will also need to consider
variables such as cost and speed.
Static Testing
Static tests analyze code without requiring the application to run. They are
the simplest type of test and the first line of defense, and you use them
primarily during the development process.
In Code Café, you implemented static testing with ESLint. Linting, as you
have seen, checks your code for common errors and pitfalls.
Advantages
Static tests are fast. They flag potential errors before the code
even runs.
Static tests give you an exact line of failure. You saw this in Code
Café when ESLint highlighted errors in Visual Studio Code with
red squiggles or displayed errors in the browser.
Disadvantages
Static tests are limited to the available code, so they can result in
false positives or negatives.
Linting, such as with ESLint, mixes code style and code quality
checks, and it can be hard to tell which is which.
Static tests are generally useful at any point in the development process.
They keep code consistent across teams and provide quick feedback when
they find potential errors.
Unit and Integration Testing
Unit and integration tests both test code in a controlled environment instead
of using a real browser with the live server. Unit tests focus on small pieces
of code in isolation, such as a function. Integration tests focus on pieces of
code that interact with other code.
You need a test runner to perform unit and integration tests. There are many
options available. The tests you write for Code Café will use Jest, a
JavaScript testing framework that comes bundled with Create React App.
There are quite a few tests that the Jest testing system can run, including
tests that target individual functions or components as well as tests for the
full app.
But suppose that Code Café later expands internationally and you localize
setFormattedPhone to format the phone number based on the user’s region.
Even if the resulting formatted phone number stays the same in most cases,
you would need to refactor your tests to include the location information to
get the expected output values.
Advantages
Disadvantage
If you later refactor the function you are testing, you might have
to refactor the tests as well.
You can add unit tests to existing code – for example, before a refactor – or
you can add them incrementally as you add new features.
Testing a component validates that what renders onscreen (and how the user
can interact with a component) matches expectations.
This is the standard mode of testing that the React Testing Library provides.
The library works with Jest to render a component and all its children into
HTML nodes – not in the browser, but to a fake DOM, using a library
called jsdom.
Advantages
Disadvantages
You cannot easily test the data passed between parent and child
components.
Using a fake DOM means that jest does not actually draw the
page on a screen. Instead, the integration test uses the HTML
output to determine what elements exist. This means that in the
fake DOM, elements are still clickable and readable even if other
elements obscure them or if they are hidden with CSS.
Do not be afraid of the longer list of disadvantages for this type of test.
Testing components is usually fast, and you can implement these tests at
any point in the development process. They are great for testing multiple
states of a component, such as a success state and an error state. And they
compensate for their disadvantages by making it less likely that refactoring
will force you to rewrite tests.
You can also test the full application using the React Testing Library, Jest,
and jsdom. Rather than rendering an individual component, these tests
render the entire application – including the router, which allows Jest to
“navigate” around the application.
Although jsdom cannot actually change pages, React Router uses the
History API to show different URLs in the address bar. This means that
tests can use React Router to “navigate” between components in jsdom, just
like users can in the browser. So these tests allow you to test complete paths
through your application.
When testing components, you might be able to avoid mocking some APIs
by providing data through props or context. But when testing the whole
application, you must mock out the API, which makes test setup more
complicated. However, these tests are much faster than end-to-end tests,
which have to start a real browser.
Advantages
Disadvantages
You need to know the full component tree to find the source of a
failure.
You can implement these tests at any point in the development process and
use them to test happy or unhappy paths through your application.
Snapshot testing
Snapshot tests detect changes in the application UI. With snapshot testing,
you avoid writing the conditions for truth and instead store the rendered
HTML as the source of truth. Then you use Jest to render the UI and
compare it with the previously stored snapshot.
This type of test relies on the assumption that the DOM was correct when
you stored the first snapshot. Also, it is important to understand that a
snapshot is not a picture – it is the HTML output. So although a CSS
change can significantly alter how the page looks, as long as the HTML
remains the same, the snapshot does not change and the test passes.
Advantages
Snapshot tests are easy to write. You need only one line of code to
assert that the current code matches the stored snapshot.
When you request code reviews from other developers, you can
include snapshot changes to show what changes you have made
(and, in fact, you must include them if you are using snapshot
tests).
Disadvantages
Advantages
You should not need to change your tests when you refactor your
front-end code.
They catch issues with obscured buttons that the fake DOM
cannot catch.
Disadvantages
End-to-end tests require you to run a web server for the browser
to load the page.
Since end-to-end tests run in a browser, you can allow them to interact with
the real back-end code.
Advantages
This kind of testing ensures that the contract between the front
end and the back end works.
Disadvantages
The live back end might be slow, leading to slow end-to-end tests.
You must set up test data and isolate the tests in the live back end.
Another option is to use mock data to run your application. Some tools,
including Cypress, allow you to mock out requests as a part of your tests.
Other tools require you to write a separate, fake back end to accomplish this
goal.
Advantage
Disadvantages
You have to maintain the mock back end.
The mock back end and the live back end might not have the
same API contract, so even though the test passes, you might
have failures in production.
Which to use?
When your team owns both the front-end and back-end code, end-to-end
testing with the live back end doubles as end-to-end testing for the back end
itself. This can save the team work because you do not have to write
separate end-to-end tests for the back end.
If your team owns only the front-end application, it can be useful to mock
the back end so that you are not blocked if the back end stops working
because of a bug or outage. Mocking the back end also lets you work ahead
of the back-end team, as long as you mock according to the API contract
that the teams have agreed on. If you choose to work ahead, make sure to
test that the front end truly integrates with the live back end before going to
production.
Visual or Screenshot Testing
A final type of testing is visual or screenshot testing. Visual tests take
screenshots of the page and compare the images for style changes. You can
write one of these tests by adding a line to match a screenshot in an end-to-
end test. As a result, visual tests come with all the same advantages and
disadvantages of end-to-end tests, and you can run them with a mocked or
live back end.
Advantages
Disadvantages
In the next two chapters, you will write various tests for Code Café.
Although you will not write every test that an application like yours might
need, you will get your feet wet and see a couple of different approaches to
testing.
Chapter 21. Testing with Jest and
the React Testing Library
In the next two chapters, you will implement some tests in Code Café.
Though all the details of testing a React app could fill a book of their own,
these chapters will give you some initial experience to build on.
In this chapter, you will use libraries included with Create React App to
write unit and integration tests.
Create React App automatically includes four libraries for testing your
application. The first is Jest, a JavaScript testing framework that includes
all the features you need to write and run tests, including a test runner, a
mocking library, and an assertion library. Jest creates a fake DOM (called
jsdom) to simulate the browser environment without rendering HTML to an
actual screen.
The second library, the React Testing Library, is a small package for testing
React components. It is one of several frameworks available from the
@testing-library suite that are designed to test applications in a way similar
to how a user would interact with them. The React Testing Library is not a
test runner; it works with Jest to create robust tests.
When testing components, the React Testing Library uses only fully
mounted components, so all child components render to the fake DOM.
Although you can test the implementation details of your components with
the React Testing Library, such as by mocking functions, looking for
element IDs, or looking for class names, that is not what the library is
intended for. Instead, the library encourages you to test how a user interacts
with your application, such as by reading text or clicking elements. This
approach helps catch bugs that directly affect the user experience.
The third library is the user-event library, also available from @testing-
library. This library coordinates with other @testing-library packages to
simulate browser interactions, such as typing or mouse clicking, in the fake
DOM.
Housekeeping
There are two pieces of housekeeping to take care of before you begin
writing tests.
"dependencies": {
...
"@testing-library/user-event": "^13.5.0",
...
}
If the version listed is lower than 14.2.0, run the following command to
update it:
Next, Create React App includes one test for you: src/App.test.js.
However, the tests in it will fail at this point. When you trigger the React
Testing Library, it runs all tests, so for the moment this guaranteed-to-fail
test will be an annoyance. Delete it for now, either by using your file
explorer or by right-clicking the file in Visual Studio Code and choosing
Delete, then confirming your intentions in the pop-up.
Before you start testing, a note about file names and locations: Jest
identifies test files by their name or their location. When running tests, Jest
includes files that end with .test.js or .spec.js, such as the
src/App.test.js file you just deleted. Jest also includes tests with the .js
extension when they are in a _tests_ folder (at any level under src).
You will follow the Create React App pattern and create test files ending in
.test.js, located at the same level as the code they test. Keeping the test
files close to the code they target helps you find them quickly, even in larger
projects, and it makes importing needed components easier.
Unit Testing
It is time to write your first test: a unit test for the cartReducer function.
This test will not render any components. Instead, it will test the output of
the cartReducer function when you give it certain parameters. Unit testing
of functions is best for functions that perform complex operations; it helps
you check whether their output matches your expectations.
describe('cartReducer', () => {
it('adds a new item', () => {
const initialCartState = [];
const itemId = 1;
const finalCartState = [{
itemId,
quantity: 1,
}];
expect(cartReducerOutput).toEqual(finalCartState);
});
});
The describe block groups related tests. (Right now, you have only one
test.) Each test within this block will test cartReducer.
The it block is the actual test that will run. The strings in the describe
block and the it block should form a statement that describes the outcome
of the test. In this case, you are testing cartReducer’s ADD action, so the
outcome statement is cartReducer adds a new item.
You list several variables within the test. The cartReducer function
executes with the initialCartState, the action type, and the itemId to add
a new item. You save the result of the function to the cartReducerOutput
variable. And the finalCartState is the expected outcome of the function.
The expect statement then uses a Jest matcher, toEqual, to assert that the
actual output of the function (cartReducerOutput) equals the expected
output (finalCartState).
So, in English, this test asserts that when you give the cartReducer
function an empty array as an initial state, the ADD action, and an itemId of
1, it returns a cart that holds a single instance of the item whose itemId is 1.
You are ready to run your test. Make sure you are in the code-cafe
directory in your terminal, and run npm test. You should see something
like this:
PASS src/reducers/cartReducer.test.js
cartReducer
✓ adds a new item (2 ms)
Watch Usage
› Press f to run only failed tests.
› Press o to only run tests related to changed files.
› Press q to quit watch mode.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press Enter to trigger a test run.
For the rest of this chapter, leave the terminal open and in watch usage
mode so you can check that your new tests pass. If you need to take a break,
you can exit watch usage mode with Control-C and restart it later with npm
test.
Next, add a test in another it block to check that cartReducer updates the
quantity of an item added to the cart.
As you did for your first test, create a variable called initialCartState.
Since this test also adds an item to the cart, you can reuse the
cartReducerOutput variable from your previous test. But this time, the
initialCartState array will not be empty; it will contain a cart item (with
a quantity and an itemId).
...
describe('cartReducer', () => {
...
expect(cartReducerOutput).toEqual(finalCartState);
});
const finalCartState = [{
itemId,
quantity: 2,
}];
expect(cartReducerOutput).toEqual(finalCartState);
});
});
...
describe('cartReducer', () => {
...
expect(cartReducerOutput).toEqual(finalCartState);
});
expect(cartReducerOutput).toEqual(finalCartState);
});
it('removes item', () => {
const itemId = 1;
const initialCartState = [{
itemId,
quantity: 1,
}];
expect(cartReducerOutput).toEqual(finalCartState);
});
});
Like your first two tests, these tests store the output of cartReducer as
cartReducerOutput and compare that value with the expected
finalCartState.
Watch usage mode runs your tests as soon as you save your file, so you do
not need to run them yourself. You should see the four strings describing
your tests and that all four tests passed:
PASS src/reducers/cartReducer.test.js
cartReducer
✓ adds a new item (3 ms)
✓ updates item quantity when adding existing item
✓ empties cart
✓ removes item (1 ms)
You could add more tests to this function, such as checking if an error is
thrown for an unknown action type or if removing an item from a cart with
multiple items works as expected. But the goal of this exercise is not to be
exhaustive, only to expose you to multiple types of testing. Check out the
challenges at the end of this chapter for more practice writing tests.
Integration Testing
Next you will write integration tests for two of your components, Thumbnail and Home. The
test for Thumbnail is very focused, and you could consider it a unit test. But as we
mentioned in the last chapter, we will call all tests of components integration tests. The test
will check whether the Thumbnail component displays the item’s title and image.
Testing Thumbnail
Put the test in a new Thumbnail.test.js file that lives alongside Thumbnail.js in the
components directory.
You will not test navigation for this test; you will test that later in this chapter, also using the
React Testing Library.
describe('Thumbnail', () => {
it('displays item title and image', () => {
render(
<Router>
<Thumbnail itemId="coffee" title="Coffee" image={itemImages.coffee} />
</Router>,
);
screen.getByText(/Coffee/i);
screen.getByAltText(/Coffee/i);
});
});
Why do you need to import MemoryRouter? The Thumbnail component uses Link, which
React Router provides. Like other React Router components, Link must be a descendant of a
Router.
MemoryRouter (which you rename Router, as you did in Chapter 9, Router) provides a fake
browser History API, since jsdom does not provide one. Wrapping Thumbnail with
MemoryRouter allows Link to render with no errors.
screen is the fake DOM that the render function populates. screen gives you access to six
query methods, including the getBy query method you use here.
getBy tells the React Testing Library that exactly one item should match the given criteria.
The getBy method throws an exception if no items match or if multiple items match.
There are several options you can pair with the query methods to find elements. You will see
many of them in this chapter. Here, you use text and altText to find elements on the
screen.
getByText and getByAltText tell the test to look for exactly one element with the text or alt
text passed to them. If either query fails, the test throws an exception.
getBy, like other query methods, searches for matches using strings, regular expressions, or
functions.
/Coffee/i is a regular expression (often called a regex). You use regular expressions in your
tests to match text without needing to worry about capitalization or whitespace.
Because an exception will make the test fail, you do not need to add an expect clause to a
query method that throws an exception. Some developers choose to include an expect
clause anyway, to make the expectation explicit. It would look like this:
expect(screen.getByText(/Coffee/i)).toBeInTheDocument();
expect(screen.getByAltText(/Coffee/i)).toBeInTheDocument();
This is a style choice; you can include expect clauses like this or not, as you prefer.
Putting it all together, this test renders a Thumbnail component with an itemId of "coffee"
and a title of "Coffee". It then uses the query method getBy to verify that the correct title
and image appear in the HTML. (Remember, these tests use a fake DOM that does not apply
styling, so the image might not actually be visible to the user.)
Save your file and check your terminal. Now you have two test suites, because you have two
files that Jest identified as test files (src/reducers/cartReducer.test.js and
src/components/Thumbnail.test.js). You should see that all the tests have passed.
Testing Home
Now let’s test the Home component. You have already tested that the individual Thumbnail
renders correctly, so the goal of this test is to check whether Home displays Thumbnails for
all the items you give it. This test will render Home and its child components, including
Thumbnail and each Link in the Thumbnails.
As with your Thumbnail test, you will not test any navigation here; you will test only the
Home component.
To find out how many Thumbnails the Home component renders, you need a way to identify
them. At the moment, there is no easy way to reference the Thumbnails without resorting to
an element selector. But the React Testing Library discourages the use of selectors such as
IDs or CSS class names, since they are not usually available to the end user. Also, you might
change or remove IDs and CSS classes for styling or other reasons.
If a test needs to access an element using a selector, the library provides a way to get
elements by test ID. A test ID is a data attribute, meaning it attaches arbitrary data to a
DOM node. The attribute’s name, data-testid, indicates to future developers that it is
related to tests in the source code.
Add the test ID attribute to the Link that the Thumbnail component returns:
...
function Thumbnail({ itemId, image, title }) {
return (
<Link
className="thumbnail-component"
data-testid="thumbnail-component"
to={`/details/${itemId}`}
...
With the test ID in place, you can write a test to render the Home component and count how
many items render with the test ID. Do this in a new components/Home.test.js file.
describe('Home', () => {
it('displays items', () => {
render(
<Router>
<Home items={items} />
</Router>,
);
const thumbnails = screen.queryAllByTestId('thumbnail-component');
expect(thumbnails).toHaveLength(items.length);
});
});
Since Home renders Thumbnail, which renders Link, you need to wrap Home in a Router just
like you wrapped Thumbnail earlier.
This time, you use the query method queryAllBy, which can return any number of matching
elements, including none. You pair the queryAllBy method with the option TestId to find
elements whose data-testid attribute matches the given string.
Unlike getBy, queryAllBy does not throw an exception if no elements match the selector; it
returns an empty array instead. Because it does not throw an exception, you must include an
expect clause. The expect clause checks that the number of items found matches the
number of items originally passed to Home.
Your test uses the more specific matcher toHaveLength. This lets Jest print more helpful
messages if the expectation fails.
For example, if the expectation fails because only 9 items render when 10 items are given,
toBeTruthy prints this message:
Expected length: 9
Received length: 10
Received array:
[
<a class="thumbnail-component" data-testid="thumbnail-component"
href="/details/bubble-tea">
<div><img alt="Tea" src="bubble-tea.svg" /></div>
<span>Tea</span>
</a>,
... // Array contents truncated for brevity
]
Having the actual array contents is helpful for debugging, and you get them automatically
when you use matchers such as toHaveLength and toContain.
To recap, this test gives the array of items to the Home component, then uses the queryAllBy
method to find the rendered Thumbnails by their data-testid attribute. It asserts that all
items render correctly by expecting that the number of rendered Thumbnails matches the
length of the items array.
Save your file and check your terminal again to confirm that the test passes.
Mocking the Server
Next, you will add integration tests for the full application. First, you will
confirm that the username displays when a user is logged in.
Since this test targets the full application as rendered by App, it should, as
much as possible, mimic how the application works in the browser.
So far, your tests have checked whether the targeted component and its
children render correctly when you provide particular props. Although App
does not have props, it does make network requests. To test the username
display functionality, you will need to mock the API request for
/api/auth/current-user.
There are a number of options for mocking API calls. For example, you can
use Jest mock functions to mock axios.get. There are also npm packages
specifically to help mock Axios.
You will use the same approach that the examples in the React Testing
Library use: a package called MSW (Mock Service Worker). This package
mocks out all the API calls with a mock server. One benefit of MSW is that if
you are developing the client app and the server is not available, you can use
the same mocks in the browser.
(To run this command, you can open a separate terminal tab or stop your tests.
If you stop your tests, restart them with npm test after installing MSW.)
Add a new directory called src/mocks. In it, create a file called handlers.js
and add your mock server handlers, as shown below:
const handlers = [
rest.get('/api/items', (req, res, ctx) => res(ctx.json(items))),
rest.get('/api/auth/current-user', (req, res, ctx) => (
res(ctx.json({ access: 'associate', username: 'Tester' }))
)),
];
MSW provides a rest method that allows you to mock GET, POST, PUT, and
DELETE calls. The handlers array is an array of requests that return the
mock response you specify. MSW also provides context (here named ctx)
that contains a variety of helper functions for things such as responding with
JSON.
You mock two GET requests – the call for items and the call for the current
user – because the main App file makes both requests.
Now create a server.js file for your mock server in the mocks directory.
Separating your handlers and server lets you use the handlers to mock the
calls in the browser, if you want to.
You set up the server using setupServer from the node folder of MSW. Then
you import the handlers you created and pass them to setupServer.
Finally, you need to modify the src/setupTests.js file that Create React
App created. This file houses any additional configuration you need when
running your tests. It already imports the @testing-library/jest-dom package,
which gives you access to custom matchers.
In setupTests.js, import your server and tell Jest how to interact with it.
Example 21.9. Modifying the setup file (setupTests.js)
Jest needs to start the mock server before it runs tests, stop it at the end of the
tests, and reset it between each test. You express this with the beforeAll,
afterAll, and afterEach blocks, respectively.
Create a new src/App.test.js file and add a test that renders App and
looks for an element with the username Tester.
describe('App', () => {
it('displays the logged in user\'s username', async () => {
render(<App />);
await screen.findByText(/Tester/i);
});
});
App already contains a router, so you can render it directly without needing
to wrap it with MemoryRouter.
Although you have mocked the API, App still loads user details
asynchronously. This means that the username Tester will not be present in
the first render. The getBy and queryAllBy methods you used in other tests
expect the element to be present immediately, so they would cause failures
here.
Instead, you use the query method findBy. Like getBy, findBy looks for
exactly one matching element. But findBy returns a promise that is fulfilled
when one element matches the selector.
findBy waits for the element to be present, so the test must await its
resolution before moving on. The promise will be rejected if findBy finds
multiple matching elements or if it finds no elements within the set timeout
period, which is 1000 milliseconds by default.
Because you use findBy’s promise, you mark the function in the it block
as async. Jest notices when an it block contains an async function; the test
automatically waits for the promise to complete and fails if the promise
fails.
As you get more experience writing tests, knowing which query method to
use will become more intuitive. The table below summarizes the available
query methods.
Resolves Failure
Query Success criteria
immediately? behavior
Throws
getBy Yes Matches exactly one element
exception
Throws
getAllBy Yes Matches one or more elements
exception
Matches zero or one element Throws
queryBy Yes
(returns null for zero elements) exception
Matches any number of elements None (no
queryAllBy Yes (returns empty array for zero failure
elements) state)
No; returns a Rejects
findBy Matches exactly one element
promise promise
No; returns a Rejects
findAllBy Matches one or more elements
promise promise
Save your file and check your terminal to confirm that App.test.js passes.
Testing Navigation
Next you will build a test that checks the user’s full path through Code Café. Your test will
check that
5. The Order Now button is disabled until the required fields are filled.
Although this test includes navigation, it still runs with Jest, the same framework you used for
the tests you wrote earlier.
Add a test to App.test.js that describes this user flow, starting with the home page check to
see if the items load correctly. Since this test will include multiple parts that check different
aspects of the user flow, use a descriptive comment to make your code easy to read and follow.
describe('App', () => {
it('displays the logged in user\'s username', async () => {
render(<App />);
await screen.findByText(/Tester/i);
});
it('allows the user to build a cart and place an order', async () => {
render(<App />);
You put the expectation in the waitFor method from the React Testing Library so that the test
will wait until there are exactly items.length Thumbnails. Like the findBy methods, waitFor
will reject the promise if the expectation is not met within the set timeout period, which is
1000 milliseconds by default. Unlike using the findBy query (which checks for exactly one
element) or findAllBy (which checks for at least one element), using expect inside waitFor
allows you to check for a specific number of elements.
Save your file and check the terminal to make sure your new test passes.
Now that you know the Thumbnails have loaded correctly, use the user-event library to
simulate clicking a Thumbnail and navigating to the details page.
When testing navigation, you need a way to verify that the test has arrived at the correct page.
You do this by looking for something that appears only on that page. What is present on the
details page and no other page in your app? The Add to Cart button. Look for it in your test.
describe('App', () => {
...
it('allows the user to build a cart and place an order', async () => {
render(<App />);
userEvent, which you import from the user-event library, lets you simulate a user action, such
as clicking an element. You pass the target element as an argument to the event function.
Here, you use the query method getBy to locate the Link element as the target element. You
get the element by its role, link – specifically, a link containing the text Tea.
The browser uses the role to determine the element’s accessibility features. Some elements,
including links, buttons, and headings, have a default role that the browser sets. You can also
assign roles to elements yourself. For example, in Chapter 16, Component Composition, you
set the role of the Alert component to alert so that screen readers can read it correctly.
The React Testing Library recommends finding elements by their role when possible.
Following this pattern in your tests can help you make sure your components have roles to
support accessibility.
After finding the correct element, the test waits for routing to occur by testing that the Add to
Cart button (which is visible only on the details page) has rendered.
Once again, save your file and confirm that all your tests are passing.
Next you want to check that when the user clicks Add to Cart, the cart quantity updates. To
do that, you will need a data-testid so that you can grab the cart quantity element. Add that
to the cart badge in Header.js.
...
function Header({ cart }) {
const cartQuantity = cart.reduce((acc, item) => acc + item.quantity, 0);
return (
...
<Link to="/cart">
<img src={CartIcon} alt="Cart" />
<div className="badge">{cartQuantity}</div>
<div
className="badge"
data-testid="cart-quantity"
>
{cartQuantity}
</div>
</Link>
...
With the data-testid in place, add to your test in App.test.js. Add two teas to the cart,
using userEvent to model clicks on the Add to Cart button, then confirm that the cart
contains the expected items.
...
describe('App', () => {
...
it('allows the user to build a cart and place an order', async () => {
...
// Clicking a Thumbnail navigates to the details page
await userEvent.click(screen.getByRole('link', { name: /Tea/i }));
await screen.findByRole('button', { name: /Add to Cart/i });
Think about what happens in your code when an item is added to the cart: The dispatch
method fires to update the state of the cart reducer, causing the affected components to re-
render. Though all of this happens pretty quickly, it is not instantaneous. You use the waitFor
method to wait until the components have updated and the expected text is visible.
toHaveTextContent is a matcher you have not used yet. It comes from jest-dom, one of the
Create React App libraries imported in src/setupTests.js. Once the test locates the cart
badge, this matcher asserts what the text content of the badge node should be.
Matchers from jest-dom assert what should be on the screen at a given time, helping you keep
your tests declarative and easy to read. You can read about toHaveTextContent and other jest-
dom matchers at github.com/testing-library/jest-dom#custom-matchers.
Save your file and check the terminal to make sure all your tests pass before moving on.
Testing checkout
After adding items to the cart, the user navigates to the cart page and checks out. Your next
task is to test that clicking the cart link takes the user to the cart page, that the Order Now
button is disabled until required fields in the order form are filled, and that clicking Order Now
displays the successful order message and empties the cart.
To do this, you will need to mock the POST request to /api/orders in the MSW handlers.
Add this change to handlers.js.
...
const handlers = [
rest.get('/api/items', (req, res, ctx) => res(ctx.json(items))),
rest.get('/api/auth/current-user', (req, res, ctx) => (
res(ctx.json({ access: 'associate', username: 'Tester' }))
)),
rest.post('/api/orders', (req, res, ctx) => res(ctx.status(201))),
];
Like the other mocked requests, this request is inside the handlers array, which you are
already exporting. You do not need any additional code to be able to use this request.
In your test, navigate to the cart page and verify the navigation by looking for something that
appears on that page and nowhere else, such as the Name label in the checkout form. Then fill
in the checkout form with a name and ZIP code (the required fields), and submit an order. Add
checks to confirm that the Order Now button is correctly disabled and enabled based on
whether the required fields are present.
...
describe('App', () => {
...
// Items are successfully added to the cart
...
await waitFor(() => {
expect(screen.getByTestId('cart-quantity')).toHaveTextContent('2');
});
// The Order Now button is disabled until the required fields are present
expect(screen.getByRole('button', { name: /Order Now/i })).toBeDisabled();
await userEvent.type(screen.getByLabelText(/Name/i), 'Big Nerd Ranch');
await userEvent.type(screen.getByLabelText(/ZIP Code/i), '30316');
expect(screen.getByRole('button', { name: /Order Now/i })).toBeEnabled();
There is a lot going on here. Though each step uses code that you have seen before, let’s break
it down line by line:
The test gets the cart link by its role and text, then clicks it (just like a user would click
the cart link).
To check that it has navigated to the cart page, the test finds the name input by its label
text.
The test finds the Order Now button by its role and text, then checks that the button is
initially disabled.
The test fills out the name and ZIP code in the checkout form, using input labels to get the
target elements.
Now that the required fields are present, the test checks that the Order Now button is
enabled.
The test clicks the Order Now button to place the order.
The test waits until the thank-you message in the Alert component is visible.
(Because the Alert component is always present on the screen, the additional matcher
toBeVisible checks the status of the hidden attribute. Like toHaveTextContent, this
matcher comes from jest-dom.)
To check that the cart has reset, the test waits until no items are showing in the cart.
Save your file and check your terminal. All your tests should still be passing.
The last part of the order submission flow that you will test is confirming that the order made
it to the server. Ideally, the test would visit the orders page and check that the order shows up
there. But mocking websockets requires other testing libraries and is uncommon enough to be
outside the scope of this book. (However, you will test the orders page in the end-to-end tests
you write in the next chapter.)
Add a new file in the src/mocks directory called data.js. Create mock order data, as shown
below:
Although this method of storing data is not persistent, it is sufficient for testing.
const handlers = [
rest.get('/api/items', (req, res, ctx) => res(ctx.json(items))),
rest.get('/api/auth/current-user', (req, res, ctx) => (
res(ctx.json({ access: 'associate', username: 'Tester' }))
)),
rest.post('/api/orders', (req, res, ctx) => res(ctx.status(201))),
rest.post('/api/orders', async (req, res, ctx) => {
addOrder(await req.json());
return res(ctx.status(201));
}),
];
You update the POST request to /api/orders, getting the order details from the request using
await req.json() and passing the result to the new addOrder function. This will store the
order in the orders variable, similar to how you would store it in a database.
Finally, update your test setup file to reset the data layer after each test.
...
import '@testing-library/jest-dom';
import * as data from './mocks/data';
import server from './mocks/server';
You use the reset function in the afterEach block to reset the data layer, in addition to the
server handlers, after each test. This keeps your tests clean and isolated so that if one test
places an order, it will not interfere with any other tests.
With the data layer set up, add one final check to the checkout test to confirm that the server
has an order.
...
import { items } from './items';
import { getOrders } from './mocks/data';
describe('App', () => {
...
// Clicking the Order Now button results in a successful checkout
...
await waitFor(() => {
expect(screen.getByTestId('cart-quantity')).toHaveTextContent('0');
});
You import the getOrders function from the data layer to read the current value of orders,
and you check that its length is 1.
Save your file and check your tests in the terminal. Everything passes! You have completed a
full happy path test for the user.
Checkout Error
But you are not done, because you do not want to test only the happy paths. Your last test
for this chapter will check that the error alert shows up if something goes wrong during the
checkout flow.
Does this mean you need to test the entire add-to-cart flow again? No way! To test the
checkout error, you only need to render Cart. Focusing on this one component allows you to
skip some flows that you already tested, such as starting on the home page and adding items
to the cart. Instead, you can mock the cart with an array and pass it as a prop to the
component.
You will still use the mock server to mock placing an order.
After setting up the mock failed request, you add some mock prop variables, including the
cart prop as an array with one item. You also mock the dispatch function with a Jest
function wrapper, which allows you to test whether dispatch was called. Although you can
provide a real implementation for dispatch, you do not expect it to be called, so you
provide a blank function.
After rendering the cart, the test fills out the checkout form, as before. But when the test
submits the form, the request to /api/orders fails because of the override you created. The
test expects the Alert component to be visible with the error message. Then, the test expects
the dispatch function not to have been called, since an error should not clear the cart.
Save your file and check your terminal for the updated test output.
Mocking console.error
When the test runs, it dumps a giant error log to the console, above the message that the
tests passed. Although this is helpful when an error is unexpected, when the error is
expected, like now, it just clutters up the console.
Mock the console in the test suite so you can clean up the output while ensuring that
unexpected errors will still log.
...
describe('Cart Errors', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
console.error.mockRestore();
});
There are a couple of other ways you could have done this.
You could have added spyOn and mockRestore at the top and bottom of the it block.
However, if you put mockRestore at the bottom of the it block and the test fails,
console.error will not be restored, which can cause you to miss legitimate errors in other
tests. You could then have used try/catch/finally syntax in the it block to ensure that the
method would be restored. But it is easier to let Jest handle things by using beforeEach and
afterEach.
Finally, you add an expect statement that asserts that there should be exactly one console
error as a result of this test.
Save your file and check the test in your terminal. It passes, displaying the normal Jest
output and no error log.
Conclusion
Now you have a unit test and some integration tests that run with Jest and
the React Testing Library.
Stop the test watcher with Control-C. Then run all your tests as they would
run in a continuous integration (CI) pipeline with CI=true npm test. Using
CI=true stops Jest from entering watch usage mode, so it will exit after
running all the tests.
PASS src/reducers/cartReducer.test.js
PASS src/components/Thumbnail.test.js
PASS src/components/Home.test.js
PASS src/components/Cart.test.js
PASS src/App.test.js
Though the time might vary slightly on your machine, at just a few seconds
long, these tests are quite fast.
If you were developing new features or refactoring your app, these tests
would give you confidence that your code changes have not broken Code
Café.
In the next chapter, you will add end-to-end tests to Code Café using
Cypress.
For the More Curious: File Structure for Test
Files
So far, you have created test files in the same directory as the file they are
testing. Another common pattern is to put the test files in a separate folder
named _tests_ (for example,
src/reducers/_tests_/cartReducer.test.js).
Jest provides the matcher toThrow to test that an error occurs as expected.
When using toThrow, you must wrap the code in a function to catch the
error, like so: expect(() => cartReducer(...)).toThrow();.
Silver Challenge: Testing Phone Number
Formatting
Test that the checkout form adds dashes to the text in the phone number
field as expected.
There are a few jest-dom matchers you can use to assert the value of the
input fields, including toHaveValue and toHaveDisplayValue.
Silver Challenge: Testing Tax Details
Test that the tax details adjust properly on the cart page when the ZIP code
changes.
Gold Challenge: Additional Login Tests
So far, you have tested your app’s login by returning a logged-in user from
the current user endpoint.
The happy path test for logging in as an associate should do the following:
Verify that the user is logged in and can see the Orders link. (The
displayed username will be the one that the mocked endpoint returns,
which might not match the one you provide for the username field.)
The happy path test for logging in as a guest should do the following:
Verify that the user is logged in but cannot see the Orders link.
The unhappy path should do the following:
Verify that the alert message shows up and displays the correct error
message.
Chapter 22. End-to-End Testing
Now that you have seen how unit and integration testing work, you are ready to move on to
end-to-end testing, which validates user workflows using an actual browser.
There are many end-to-end testing solutions. One of the most popular tools for testing
front-end applications is Cypress. In this chapter, you will use Cypress and the Cypress
Testing Library to write end-to-end tests.
Like Jest and the React Testing Library do for unit testing, Cypress and the Cypress Testing
Library work together to allow you to write and run end-to-end tests. Cypress runs tests in
a browser window, replicating real user interactions. The Cypress Testing Library is part of
the same family of frameworks as the React Testing Library, so you can reuse testing
patterns when writing unit and end-to-end tests.
Cypress takes a few minutes to install, so do not worry if the install command looks stuck.
Configuring scripts
The command to start Cypress from your terminal requires you to target the full path, like
this:
./node_modules/.bin/cypress open
Instead of typing out the target path every time you want to run Cypress, it is easier to use
a script that you can run in your application’s root directory. You might recall that you
added a script to run ESLint in Code Café back in Chapter 5, Linting.
In the scripts block in package.json (the same place you added your previous script),
add the two scripts you need to run Cypress.
The first script, cypress:open, opens Cypress and allows you to run tests one at a time.
The second script, cypress:run, runs all the tests in headless mode. (This mode is mostly
meant for continuous integration, or CI.)
Cypress takes a few seconds to open, then displays the Cypress welcome window
(Figure 22.1, “Cypress welcome window”). (If it crashes with an error telling you that
Cypress verification timed out, try the command again.)
The next screen shows a list of files that Cypress has added to your project (Figure 22.2,
“Cypress configuration files”).
Click Continue. On the next screen, you can choose the browser you want to use to run
your tests (Figure 22.3, “Choosing a browser”).
The final setup screen gives you options to begin creating test files, or specs (Figure 22.4,
“Cypress spec window”).
Next, you need to set up ESLint to check that the new files Cypress created – and the new
tests you will write – conform to Code Café’s lint rules.
In Visual Studio Code’s explorer, locate the directory called cypress and the
cypress.config.js configuration file that Cypress created for you. They are in your
project’s top-level directory, along with the src directory and the package.json file.
Open cypress.config.js. ESLint identifies several lint errors (Figure 22.5, “Lint errors in
cypress.config.js”):
Now, in a new terminal tab, run npm run lint in the Code Café directory. The linting
script reports no errors. Why the discrepancy?
Although the ESLint extension you added to Visual Studio Code checks all the files in the
project, the linting script you set up when you started working on Code Café checks only
files in the src directory. The cypress directory and the cypress.config.js file are
outside of src, so the command does not check them.
You will add the end-to-end tests you write to the cypress directory, so it is important to
have ESLint enforce code rules there. Update ESLint’s configuration in package.json so
that it lints the cypress.config.js file and cypress directory files as well.
...
"scripts": {
...
"test": "react-scripts test",
"lint": "eslint src --max-warnings=0",
"lint": "eslint cypress.config.js cypress src --max-warnings=0",
"eject": "react-scripts eject"
...
Run npm run lint again in your terminal. Now the output reports several errors:
/.../code-cafe/cypress.config.js
1:34 error Strings must use singlequote quotes
5:21 error 'on' is defined but never used no-unused-vars
5:25 error 'config' is defined but never used no-unused-vars
/.../code-cafe/cypress/support/commands.js
25:78 error Newline required at end of file but not found eol-last
/.../code-cafe/cypress/support/e2e.js
17:20 error Missing semicolon semi
20:25 error Newline required at end of file but not found eol-last
There are six problems in all. Four are auto-fixable errors related to three ESLint rules:
These errors are related to code style. Fixing them will not affect your app’s behavior, so
they are OK to auto-fix. Now run the command to auto-fix the errors: npm run lint -- -
-fix.
Rerun npm run lint. The remaining two errors highlight unused variables in
cypress.config.js:
> eslint cypress.config.js cypress src --max-warnings=0 --fix
/../code-cafe/cypress.config.js
5:21 error 'on' is defined but never used no-unused-vars
5:25 error 'config' is defined but never used no-unused-vars
You cannot auto-fix the no-unused-vars rule – you have to manually update the file to use
the variables, delete the code that creates them, or ignore the lint rule. Although you are not
using these variables now, you might need them as you add custom configurations to your
Cypress setup, so your best option is to exempt them from the lint rule.
Open cypress.config.js and disable the no-unused-vars rule with the comment line you
have used before.
module.exports = defineConfig({
e2e: {
// eslint-disable-next-line no-unused-vars
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
});
Now you have resolved the current linting errors. Next, you need to set up ESLint to make
use of the Cypress plugin.
In the cypress directory, create a file called .eslintrc.js. (Do not forget the dot at the
beginning of the filename. The dot identifies it as a configuration file. By default, files
beginning with a dot are hidden.)
module.exports = {
extends: ['plugin:cypress/recommended'],
};
ESLint allows you to override and extend your configurations by directory. Here, you add
Cypress-specific rules from the Cypress ESLint plugin that will apply only within the
cypress directory. These rules will enforce best practices recommended for writing
Cypress tests.
Now you can continue your work without any complaints from ESLint.
Importing commands
The commands that Cypress provides are like statements that describe the interaction
happening during a particular test. The Cypress Testing Library provides additional
commands, such as findBy, that are similar to the query methods you used in the last
chapter. To use the commands from the Cypress Testing Library, add an import statement
to cypress/support/commands.js.
...
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands';
There is one final piece of configuration you need before writing your first test: setting the
app’s base URL. Do this in cypress.config.js.
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
// eslint-disable-next-line no-unused-vars
setupNodeEvents(on, config) {
...
Setting the baseUrl makes it easy to change the expected URL of the application. It also
saves you some time when you write your tests. Instead of writing out the URL each time,
like this:
cy.visit('http://localhost:3000/details/coffee')
you can omit the base URL and write just this:
cy.visit('/details/coffee')
Testing the Login Flow
By default, Cypress looks for test files that match the following pattern:
cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
Create a subdirectory in cypress called e2e to house your end-to-end test files.
In the cypress/e2e directory, add a test file called login.cy.js.
Your first Cypress test will validate the user’s login flow. Add a test that starts
at the home page and tests that a user can log in successfully, as shown below.
describe('login', () => {
it('shows logged-in user\'s username', () => {
cy.visit('/');
cy.findByRole('link', { name: /Log In/i }).click();
cy.findByLabelText(/Username/i).type('Tester');
cy.findByLabelText(/Password/i).type('pass');
cy.findByRole('button', { name: /Log In/i }).click();
cy.findByRole('link', { name: /Log In/i }).should('not.exist');
cy.findByText(/Tester/i);
});
});
Cypress uses the same structure for test files as Jest does, with describe and
it blocks, so this code looks similar to the tests you wrote in the previous
chapter. Using the Cypress Testing Library also allows you to use the familiar
findBy and findAllBy methods to query for elements.
getByand getAllBy throw exceptions if the criteria are not met, so these
methods are not retryable, and Cypress does not support them.
What about queryBy and queryAllBy? To get the same results with Cypress,
you use a modified chainer, like this:
cy.findByText(...).should('not.exist')
Using Cypress to interact with the page is a bit different from using Jest. With
Cypress, you identify the target element, then chain the Cypress user event
methods. The chainer click simulates clicking on an element, while type
simulates typing, with the value to type passed in as the argument.
starts by visiting the home page at / (taking advantage of the base URL
you set)
Save all your files, and let’s run your first test.
Make sure your Code Café front end and back end are running in separate
terminal windows or tabs. In your browser, visit Code Café at
http://localhost:3000 if it is not already open.
In a third terminal window, run npm run cypress:open. The Cypress window
opens to the initial welcome screen. Select E2E Testing and then Start E2E
Testing in Chrome, as you did before. The Chrome window that launches
will display a list of test files – currently, a list of one (Figure 22.6, “Cypress
test file”).
Figure 22.6. Cypress test file
Click login.cy.js to run the test in the browser (Figure 22.7, “Cypress
successful run”).
(Cypress uses a dark theme by default. For the screenshots in this book, we
used the tool available at www.npmjs.com/package/cypress-light-theme to run
Cypress in a light theme.)
Cypress uses green for passing tests and red for failing tests, so the green bar
alongside your test code indicates that it has passed.
Testing the Checkout Flow
Next, test the checkout flow in a new cypress/e2e/checkout.cy.js file.
In the previous chapter, you wrote an integration test for the checkout process using a
mocked server. Take a look at your code in src/App.test.js. Copy the relevant
portion of it into your new file and see if you can update it to work with Cypress.
For your testing flow, start at the home page, add two of an item to the cart, fill out the
checkout form with a name and ZIP code, submit the order, and confirm that the correct
alert displays and the cart empties. Also, check that the Order Now button is disabled
and enabled correctly. Do not worry about testing that the server received the order.
describe('checkout', () => {
it('user can build a cart and place an order', () => {
cy.visit('/');
cy.findAllByTestId('thumbnail-component').should('have.length', 10);
cy.findByRole('link', { name: /Tea/i }).click();
cy.findByRole('button', { name: /Add to Cart/i }).click();
cy.findByTestId('cart-quantity').should('contain', '1');
cy.findByRole('button', { name: /Add to Cart/i }).click();
cy.findByTestId('cart-quantity').should('contain', '2');
cy.findByRole('link', { name: /Cart/i }).click();
cy.findByLabelText(/Name/i);
cy.findByRole('button', { name: /Order Now/i }).should('be.disabled');
cy.findByLabelText(/Name/i).type('Big Nerd Ranch');
cy.findByLabelText(/ZIP Code/i).type('30316');
cy.findByRole('button', { name: /Order Now/i }).should('be.enabled');
cy.findByRole('button', { name: /Order Now/i }).click();
cy.findByText(/Thank you for your order/i).should('be.visible');
cy.findByTestId('cart-quantity').should('contain', '0');
});
});
Most of this code looks just like the test from the React Testing Library. To make the
code retryable, you replace getBy with findBy. You also use click and type chainers
for user interactions and should chainers in place of assertions.
on the details page, finds and clicks the Add to Cart button
verifies that the Alert component with the thank-you message is visible
verifies that the cart is empty by asserting that the cart icon displays 0
Click Specs at the top of the window to view the available test files (Figure 22.8,
“Expanding the specs”).
Click checkout.cy.js to run the new test (Figure 22.10, “Checkout test success”).
In your test, you check for the name field using cy.findByLabelText(/Name/i).
Although Cypress has an assertion for .should('exist'), findBy throws an error by
default if the item does not exist, so these two lines are equivalent:
cy.findByLabelText(/Name/i);
cy.findByLabelText(/Name/i).should('exist');
Feel free to try out both versions. You will notice a slight difference in output, because
the second version will show an assert (Figure 22.11, “assert in the test results”).
If you change the search text to make the command fail, you will get the same error
message either way (except that the second version will show an assertion, as above). If
you find it more readable to include .should('exist') on find commands to make the
assertion explicit, feel free to do so.
Cypress supports many assertions from Chai, Sinon, and jQuery. You can read about
them here: docs.cypress.io/guides/references/assertions.xhtml.
Interacting with the server
The end-to-end tests in this chapter use the real server to test the integration between
the server and the client.
This Cypress checkout test, like the one you wrote with Jest, checks the number of
items the café offers. Your Jest test got the number from the mock data
(items.length), so it knew exactly how many items to expect. In your Cypress test, the
items load from the real server instead. Though the server does currently return 10
items, you might add more items to Code Café in the future.
The most useful tests are not those you write and run once, but rather those that
continually check your code base for issues throughout development. It is better not to
assert a specific number of items in this test, so that you do not have to update it when
you add or remove items from the server.
describe('checkout', () => {
it('user can build a cart and place an order', () => {
cy.visit('/');
cy.findAllByTestId('thumbnail-component').should('have.length', 10);
cy.findByRole('link', { name: /Tea/i }).click();
...
Cypress also supports mocking the server, in which case it would be safe to assert a
specific number without risking a test failure if the server changes. But the trade-off is
that you would no longer be testing server-client integration.
Now that you’ve tested the checkout happy path, create a test for checkout errors in a
new it block in checkout.cy.js.
Unfortunately, Cypress cannot start with a mock cart, so your new test will have to
build the cart out again. However, you can reuse your earlier code to start at the home
page, add a tea to the cart, navigate to the cart page, enter a name, and click the Order
Now button.
This time, enter the ZIP code 99999 to trigger an error from the server, and then check
for the correct alert message.
Example 22.10. Cart error test (checkout.cy.js)
describe('checkout', () => {
it('user can build a cart and place an order', () => {
...
});
This test uses simpler code to put an item in the cart, since the happy path test already
checks that the cart badge updates with the correct quantity for multiple items.
Save your file and run the updated checkout.cy.js test in Cypress. It passes.
Although Cypress does not show the console error by default, it would still be nice to
assert that the error logged. Like Jest, Cypress can spy on the console.
describe('checkout', () => {
it('user can build a cart and place an order', () => {
...
});
Save your file and run your test again to confirm that it still passes (Figure 22.12,
“Passing checkout tests”).
Add a new file in your e2e directory called orders.cy.js to test the flow for
viewing and deleting an order. Since only associates can view and delete
orders, you will need to begin by logging in an authorized user. For now,
duplicate the login flow from your login test. Then add a tea to the cart and
check out. You can also duplicate most of the checkout flow from previous
tests. (Later, you will factor out both duplicate flows.)
(This test will fail if you run it. You will fix it in the next step.)
describe('orders', () => {
it('user can view and delete orders', () => {
cy.visit('/');
cy.findByRole('link', { name: /Log In/i }).click();
cy.findByLabelText(/Username/i).type('Tester');
cy.findByLabelText(/Password/i).type('pass');
cy.findByRole('button', { name: /Log In/i }).click();
cy.findByRole('link', { name: /Tea/i }).click();
cy.findByRole('button', { name: /Add to Cart/i }).click();
cy.findByRole('link', { name: /Cart/i }).click();
cy.findByLabelText(/Name/i).type('Big Nerd Ranch');
cy.findByLabelText(/ZIP Code/i).type('30316');
cy.findByRole('button', { name: /Order Now/i }).click();
cy.findByRole('link', { name: /Orders/i }).click();
cy.findByRole('button', { name: /Delete Order/i }).click();
cy.findByText(/No Orders/i);
});
});
After logging in and placing an order, the test clicks the Orders link to go to
the orders page. From the orders page, the test clicks the button to delete the
order. Since the test placed only one order, it asserts that the text “No Orders”
exists on the page.
From the Cypress window, run your orders.cy.js file. The test fails
(Figure 22.13, “Delete Order failure”).
Why did it fail? Scroll up in the Cypress output to find the beginning of the
red text. The error says the test failed because it found multiple elements with
the role "button" and the name Delete Order. findBy expects to find
exactly one matching element; if there is more than one, it throws an error.
Although this test places only one order, you can see in the browser window
on the right that there are several existing orders, each with a Delete Order
button. Why? Because the server is not cleared between test runs, it sees the
orders from all previous test runs.
You might think that a quick fix for this issue would be to restart the server
before each test run. Although this would clear existing orders from previous
runs, a single run can contain multiple tests that place orders.
It is important to isolate the conditions needed for a test to run so that it does
not interfere with other tests. Before this particular test starts, you need to
clear the orders to ensure that no previous orders are hanging around.
(As your application grows, you might want to run tests in parallel to save
time and resources. This would involve splitting up your test suites and
running them separately but simultaneously. You would need to take
additional steps to ensure that no new orders are placed while you test the
orders workflow. But for this book you will continue to run your tests in
sequence, so you do not need to worry about this.)
The API includes an endpoint for deleting all orders. Cypress can send
network requests, so make a DELETE request to /api/orders to erase all the
orders at the beginning of the orders test workflow.
describe('orders', () => {
before(() => {
cy.request('DELETE', '/api/orders');
});
it('user can view and delete orders', () => {
...
In Cypress, the hook that runs once before all the tests is named before, not
beforeAll as it is in Jest. (Cypress also has a beforeEach, which behaves
like beforeEach in Jest.)
Save your file and run your orders.cy.js test again. This time, it passes.
Factoring Out Custom Commands
In writing your three end-to-end tests, you have used some of the same workflows over and
over. Duplicating code is tedious and makes your testing code (or any code) harder to read
and maintain. To avoid repeating common bits of code, Cypress allows you to create
custom commands.
Open the file cypress/support/commands.js, where you imported the command set from
the Cypress Testing Library. This file includes examples to follow when writing new
commands or overwriting existing commands.
Parent commands start a new command chain. Child commands, such as the click chainer,
chain off a parent or off another child command. Dual commands, such as the contains
chainer, combine elements of both parent and child commands; you will rarely use them.
At the bottom of the file, add a command for logging in to Code Café, using the workflow
duplicated in the login and orders tests. Since this will be a new command chain, follow
the sample code for creating a parent command.
...
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands';
The custom login command is a JavaScript function, where the first argument is the name
of the command, and the second argument is a callback function. You can also pass
arguments to the callback function. You need different usernames for your tests, so you
allow the caller to specify the username and password, and you provide default values if
the caller does not specify them.
Now replace the workflow in the login test with your new command, using the default
values for the username and password.
describe('login', () => {
it('shows logged-in user\'s username', () => {
cy.visit('/');
cy.findByRole('link', { name: /Log In/i }).click();
cy.findByLabelText(/Username/i).type('Tester');
cy.findByLabelText(/Password/i).type('pass');
cy.findByRole('button', { name: /Log In/i }).click();
cy.login();
cy.findByRole('link', { name: /Log In/i }).should('not.exist');
...
Save your files. In the Cypress window, run the login spec again to confirm that it still
passes.
You also have a shortened checkout workflow that you duplicate in the checkout error test
and the orders test.
Add a new custom command for checking out. Allow the caller to specify the ZIP code.
(This is not intended to replace the happy path workflow for checking out, which is more
in depth.)
...
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import '@testing-library/cypress/add-commands';
Now replace the workflow in the checkout test with the new command.
describe('checkout', () => {
...
it('shows an error when the order fails', () => {
...
});
cy.findByRole('link', { name: /Tea/i }).click();
cy.findByRole('button', { name: /Add to Cart/i }).click();
cy.findByRole('link', { name: /Cart/i }).click();
cy.findByLabelText(/Name/i).type('Big Nerd Ranch');
cy.findByLabelText(/ZIP Code/i).type('99999');
cy.findByRole('button', { name: /Order Now/i }).click();
cy.checkout('Big Nerd Ranch', '99999');
cy.findByText(/There was an error/i);
...
You pass the username Big Nerd Ranch and the ZIP code 99999 to the command as
arguments to overwrite the default values.
Save your files and run the checkout test again to confirm that it passes.
Next, refactor the orders test to use both of your custom commands.
describe('orders', () => {
...
it('user can view and delete orders', () => {
cy.visit('/');
cy.findByRole('link', { name: /Log In/i }).click();
cy.findByLabelText(/Username/i).type('Tester');
cy.findByLabelText(/Password/i).type('pass');
cy.findByRole('button', { name: /Log In/i }).click();
cy.findByRole('link', { name: /Tea/i }).click();
cy.findByRole('button', { name: /Add to Cart/i }).click();
cy.findByRole('link', { name: /Cart/i }).click();
cy.findByLabelText(/Name/i).type('Big Nerd Ranch');
cy.findByLabelText(/ZIP Code/i).type('30316');
cy.findByRole('button', { name: /Order Now/i }).click();
cy.login();
cy.checkout();
cy.findByRole('link', { name: /Orders/i }).click();
...
You can replace a lot of code with these two commands. Custom commands save you
typing and allow you to focus on the unique workflows in each test.
Save your files and confirm that all your tests still pass.
Invisible Elements
One of the main benefits of running end-to-end tests in a browser is that the
browser actually draws the elements on the page. This means that CSS
applies.
Let’s make a couple of edits to prove this. First, hide the checkout inputs with
CSS:
...
.cart-component label {
...
}
.cart-component input {
display: none;
border-radius: 5px;
...
Save your file and, in a new terminal tab, run your Jest component tests (npm
run test). They still pass: Even though the inputs would not be visible to the
user, the tests run through all the workflows.
Now run checkout.cy.js in the Cypress window. This test fails, and Cypress
displays a helpful error message about why it could not type in the input field
(Figure 22.14, “Cypress test failure”):
Cypress allows you to disable error checking for hidden elements with {
force: true }. But generally, you do not want to disable it, so you can catch
bugs such as this one.
...
.cart-component label {
...
}
.cart-component input {
display: none;
border-radius: 5px;
...
Although the three tests do not take very long to run, they are noticeably
slower than the unit and integration tests from the last chapter. For a larger
application, the extra time would be much more noticeable.
One reason for this is that your end-to-end tests have to execute most of the
checkout flow to set up for other workflow tests. In the integration tests, you
pass in a mock cart prop, which is faster.
Another reason is that the end-to-end tests have to start a real browser for
each test, and that takes far more time than using jsdom.
So, why use end-to-end tests at all? Because these tests use the real server,
they help test the integration between the server and your React application.
Even if the tests mock the server, they help you check that elements are
actually visible, which is possible only when the browser actually draws the
page. Cypress can also test in multiple browsers, in case they differ in how
they they draw the page or other behaviors.
In many cases, you can write most of the tests you need as integration or unit
tests, which are faster than end-to-end tests. Then you can write just a few
end-to-end tests for testing the integration with the server and focusing on
important workflows to catch covered elements.
In the next chapter, you will wrap up your work on Code Café by building the
app for production use.
For the More Curious: Cypress Media Files
When Cypress runs in headless mode, it saves screenshots and videos. You
should not commit these files when using version control.
Create React App created a file for you called .gitignore, which lists files
that Git should not commit to version control. In this file, you can list
directories or specific files or patterns to ignore. You can also include
.gitignore files in subdirectories to extend the rules. Adding the following
lines to the .gitignore file will ensure that Git does not commit the
Cypress media directories:
# cypress
/cypress/screenshots
/cypress/videos
It can send requests to set up test data, as you saw in this chapter when you
used a DELETE request to clear the cart. It can also wait for requests to
complete, so you can verify that a network request is done before looking at
the browser for updates. This is particularly helpful for slower requests that
might otherwise time out.
Cypress can also make assertions with network requests, so your test that
places an order could have made a GET request to /api/orders to check
that the order made it to the server.
And, if needed, Cypress can stub requests so it does not make actual server
calls at all.
When you are done, compare your code with the Jest version you wrote for
a similar challenge in the previous chapter.
Silver Challenge: Testing Cart Details
Test with Cypress that the tax details adjust properly when the ZIP code
changes.
Also, check that adding different items and multiples of the same item both
work as expected.
When you are done, compare your code with the Jest version you wrote for
a similar challenge in the previous chapter.
Silver Challenge: Testing Removing an Item from
the Cart
Test with Cypress that the button to remove an item from the cart works as
expected.
When you are done, compare your code with the Jest version you wrote for
a similar challenge in the previous chapter.
Gold Challenge: Additional Tests
Implement any other tests that you can come up with, using either Cypress
or Jest.
Chapter 23. Building Your
Application
Now that Code Café is fully coded and (for the purposes of this book) fully
tested, you are ready to build it for production use.
So far, you have done all your work in development mode, using npm start
to run the server built into Create React App. By the end of this chapter, you
will have a folder of files ready for you to deploy onto a production server.
Manifest
First, you need to update the application’s public/manifest.json file. This
file contains information that the browser will consume. Locate and open
the file in Visual Studio Code.
{
"short_name": "React App",
"short_name": "Café",
"name": "Create React App Sample",
"name": "Code Café",
"icons": [
...
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"theme_color": "#B9A28D",
"background_color": "#ffffff"
}
The short_name field is used if there is not enough space to display the full
name – for example, on the home screen of a phone.
An app’s name should not exceed 45 characters, and the short name should
not exceed 12 characters. You use Code Café as the app name and Café as
the short name. You could also have used a verbose app name such as Big
Nerd Ranch Presents: Code Café and then used Code Café as the short
name.
If the user loads the app from a bookmark, the background_color will
show before the app loads the stylesheet. Since the background for Code
Café is white, no changes are needed.
When the build completes, you will see Compiled successfully. followed by
information about file sizes and details about deployment options:
Compiled successfully.
63.16 kB build/static/js/main.e09ba5e9.js
1.93 kB build/static/css/main.e5132e68.css
1.79 kB build/static/js/787.4539c236.chunk.js
https://cra.link/deployment
(The file sizes you see might differ from the ones listed above.)
Look back in Visual Studio Code. Now there is a build directory, which contains
all the files you need to deploy your website to production.
Expand the build directory to see its contents. Next, expand the static
subdirectory and the css, js, and media directories under it.
Every file in these directories has a hash in its name. This hash stays constant as
long as the file does not change, and it updates if the file does change. This means
that the files under the static directory are safe for users and content-delivery
networks, or CDNs, to cache for long periods of time.
What do these files actually contain? The main.*.css file (where * takes the place
of the hash in the filename) has the combined and minified result of all the CSS
files you imported. The main.*.js file has the combined and minified version of
the JavaScript your application needs. In 787.*.js (which might begin with a
different number), you will find web-vitals, a package that Create React App
imports in src/reportWebVitals.js.
How does the webpack build process decide what JavaScript your application
needs? Starting with src/index.js, it traverses all the imported files and
dependencies and places them in main.*.js. This means that if you install a
dependency and never use it, it will not end up in the file. It also means that your
test files (such as App.test.js), test dependencies (such as @testing-
library/react), and build dependencies (such as ESLint) do not end up in
main.*.js, because nothing under src/index.js imports them.
To deploy Code Café, you would need some additional code on the server
to support client-side routing. You would also need to deploy the back-end
server and proxy the API and websocket requests.
It is outside the scope of this book to cover all the steps you need to deploy
Code Café online. To learn more about options for deployment, follow the
link in your terminal output from after you built the app:
cra.link/deployment.
Conclusion
You did it! Code Café is complete.
Code Café includes prop types to help future developers understand the
application and ESLint to enforce code style. You used useReducer to
handle complex state for the cart. You used React Router and conditional
rendering to create your application with multiple pages that respond to
changes in that cart state. And you used additional hooks such as useState,
useEffect, and useRef to interact with the server, respond to user events,
and collect input with forms.
You also added advanced features to your application by sharing state with
context and composing components to make alerts for the cart. And you
optimized your application with memo, useMemo, and useCallback.
In the next two chapters, you will leave Code Café behind and learn more
about performance tuning in React – with the help of some playful
penguins.
Chapter 24. Data Loading
In the last two chapters of this book, you will investigate several issues that
affect app performance and learn about more ways to optimize your apps.
You will not build your own app in this section of the book; instead, you will
work with an app we provide for you in your downloaded resources file.
In this chapter, you will examine the amount of data coming across the
network and its effect on performance. You will also see two ways you can
improve the performance of apps: keeping an eye on the size of image files
and using lazy loading for large files that you do not need throughout the app.
If you run into any issues with this app, check the forum for this book for
help.
Once the app starts, you will see the home page of the Performance Penguins
website in your browser (Figure 24.1, “Welcome!”):
Exit the current server by pressing Control-C in the terminal window. Run
npm run serve to serve a production build, and visit http://localhost:3001 in
your browser.
Let’s see how the website loads for a user on a 3G network connection.
Open the DevTools to the Network tab. Change the throttling setting to Fast
3G and check the box for Disable cache (Figure 24.2, “Setting network
throttling”).
Now refresh the page. It spends a long time on a blank white screen while it
loads the content.
Look at the bottom of the Network tab for some statistics about the load time
(Figure 24.3, “Network statistics”):
To load the home page for your application, the browser has to load 2.5MB
over seven requests. It takes 8.90 seconds to initially load the DOM and
15.73 seconds before it finishes. (As usual, your results might be different.)
Look at the individual requests to get a better idea of what the browser is
loading (Figure 24.4, “Network requests”). (You might need to set the filter
to All to see the requests.)
Figure 24.4. Network requests
The table below describes each file. (As in Chapter 23, Building Your
Application, the * in each filename takes the place of the generated hash.)
File Description
The index.xhtml file, which provides the
localhost
base HTML.
The compiled JavaScript for the React
main.*.js
application.
main.*.css The compiled CSS for the website.
penguinFull.*.jpg The image in the header.
favicon.png The site’s favicon.
manifest.json The site’s manifest.
logo192.png The site’s 192 × 192 px logo.
You might not see this file, which is injected
react_devtools_backend.js by the React DevTools if they are enabled.
You can safely ignore it.
Looking at the file sizes, the biggest files are the penguin image at 1.0MB
and main.*.js at 1.4MB.
Large Images
The first thing to address is the large size of the penguin image. Click the
network request for the image to get more details. Switch to the preview pane
and check out the dimensions of the image in the footer (Figure 24.5,
“Penguin image dimensions”).
Now let’s look at where it renders in the header. Right-click the penguin
image in the header and select Inspect. Then select the computed pane in the
DevTools (Figure 24.6, “Image element dimensions”).
function Header() {
...
After making this edit, you will need to exit the existing server with Control-
C and rerun npm run serve to rebuild the production site.
Next, navigate back to the Network tab and refresh the page (Figure 24.7,
“Network requests with a smaller image”).
Now the Network tab shows penguinLogo.*.jpg, which is only 25KB. And
with just that change, you have reduced the finish time by 5 seconds; now it
is 10.6 seconds.
There are also some build plugins that can automate resizing for you. If your
project does not use one of these tools, it is important for you as a developer
to pay attention to the file size and dimensions of the images provided to you.
If you get images that are too large, work with your team and designers to get
smaller images.
If you are working with scalable vector graphics, or SVGs, there are a couple
of things to consider. First, SVGs do not have a fixed size; instead, their
contents specify a path to draw with a certain precision based on decimal
points. Design tools often generate SVGs with a larger number of decimal
points than necessary for displaying the image on a website. You can use an
SVG optimizer to reduce the number of decimal points without otherwise
harming the image.
Also, it is worth looking at the source of SVGs you receive for a project.
Occasionally, a generated SVG contains only a JPG or PNG in hexadecimal.
This means the file is larger than necessary, because it is no longer in binary –
and it is not actually a vector graphic.
Analyzing JavaScript Files
The next thing to look at in Performance Penguins is why the main.*.js file
is 1.4MB.
Since the build prepared this file for production, opening it and reading the
source is unlikely to be useful. However, there are other ways to analyze a
file.
Your production build created a .js.map file, and your downloaded project
has source-map-explorer, an npm package that helps analyze .js.map files.
In your terminal, stop the app with Control-C, then run npm run analyze.
Your browser will open to a page that looks like Figure 24.8, “JavaScript file
analysis”:
Next, look at blocks with only a few children. These children are often npm
packages (Figure 24.10, “Analysis of packages”).
The home page does not need the data file for the Penguin Data page or the
animations for the Penguin Game page. How can you change the code so the
home page loads faster – even on a slow connection?
Lazy Loading
The solution is lazy loading, a technique that allows your application to load
components only when it needs them. This is also known as code splitting.
You will implement lazy loading in Performance Penguins so that the data
file and animations will load not with the home page, but only when the app
needs them.
You will also use special import syntax to tell webpack that you want to load
an import asynchronously. This is called a dynamic import. Dynamic imports
invoke import as a function. When you invoke import, it returns a promise
that resolves when it receives the file.
To add lazy loading, you will use two new React features, lazy and
Suspense. lazy is a function for rendering a dynamic import as a component.
It accepts an arrow function, which it invokes when a caller wants to render
the component onscreen.
When <Thing /> first renders, React invokes the dynamic import to load the
code for the Thing component. Once the code loads, <Thing /> will render
onscreen.
So what happens while <Thing /> is loading? This is where the React
component Suspense comes in: Suspense provides fallback code for the lazy-
loaded component.
You place Suspense higher in the component tree than the lazy-loaded
component. During loading, React navigates up the component tree from the
lazy-loaded component to find the closest Suspense component. Then it
replaces all of the Suspense component’s descendants with the fallback code.
Once loading is complete, the descendants and the newly loaded component
display onscreen.
In Performance Penguins, update App.js to use lazy loading. (You will need
to reindent the code nested in Suspense.)
import {
...
} from 'react-router-dom';
import { lazy, Suspense } from 'react';
import Header from './components/Header';
import NotFound from './components/NotFound';
import Home from './components/Home';
import Data from './components/Data';
import Game from './components/Game';
function App() {
return (
<Router>
<Header />
<Suspense fallback={<div>Loading...</div>}>
<Routes>
...
</Routes>
</Suspense>
</Router>
...
Now you are lazy loading two components, Data and Game. Let’s see what
this change does to the site’s loading time.
Restart your server with npm run serve. Refresh the home page and check
the Network tab (Figure 24.11, “Network requests with lazy loading”).
Leave the Network tab open and click the Penguin Game link in the website’s
header. You will see Loading… on the page while it fetches the content, then
the Penguin Game will appear.
Look back in the Network tab: Two new .chunk.js files have loaded
(Figure 24.12, “New .chunk.js files”).
Before you added lazy loading, clicking the link would have instantly loaded
the game, because all the files loaded with the home page. Now, these two
files wait and load only when the game page loads.
So lazy loading involves a trade-off: Although the home page loads more
quickly, now other pages take longer to load. However, React keeps each
page in memory after it loads, so now you can quickly navigate between the
home page and the Penguin Game without waiting for files to load again.
Incidentally, you might have noticed that the penguinLogo image reloaded
when you navigated to the game page. This happened because you disabled
caching; it would not happen to normal users.
Naming the bundles
The names of the bundles and resulting .chunk.js files are currently just
hashed values. For development and analysis purposes, it can be nice to know
what is inside each chunk. You can use special comment syntax to tell
webpack what to name the chunks. The comment looks like this:
/* webpackChunkName: "modulename" */
You can also assign the same name to two imports, and webpack will group
them into a single chunk.
...
import NotFound from './components/NotFound';
import Home from './components/Home';
function App() {
...
Stop and restart your server. In the browser, return to the home page, then
click the link in the header to reopen the Penguin Game.
Check out the Network tab. As before, two chunk files load. Now one of
them uses the name you set: game.*.chunk.js. Why does the other chunk
file still have a generic name (Figure 24.13, “Network chunk names”)?
Figure 24.13. Network chunk names
Most packages that list their size will also include the compression format.
The most common compression format is currently gzip. However, Brotli is
smaller, and most browsers support it.
It can be confusing for a library that advertises itself as less than 25KB,
minified and gzipped, to show up in your analysis as over 50KB. What you
see includes minification but not compression.
Assuming your server uses compression, the compressed size is what will
actually make it to your user. Therefore, that size is most important when
trying to meet a benchmark loading speed.
It often makes sense to combine all the components of a single flow, such as
checkout, into a single bundle, rather than using a bundle for each
component.
Conclusion
Now Performance Penguins’ pages load only the data necessary to display
them, and the header image is much smaller.
Before you wrap up your work in this chapter, go back into the Network
tab, uncheck the box for Disable Cache, and change the throttling setting
from Fast 3G back to No Throttling.
You will need to install source-map-explorer and add the analyze script to
package.json:
Also, you will need to run npm run build after each adjustment to get the
most up-to-date analysis. (For Performance Penguins, npm run serve also
builds the app, so you do not have to run the builds separately.)
Chapter 25. Component Speed
In the last chapter, you looked at how loading data affects your app’s
performance. In this final chapter, you will look at another aspect of
performance: the speed at which components display on the screen, both
when they initially render and when they update. To do this, you will use the
React DevTools Profiler tool that you first saw in Chapter 19, Introduction to
App Performance Optimization.
Throughout this chapter, the exact render times will vary between your
machine and what this book shows. That is OK. Focus on the relative scale
between fast and slow, and pay attention to the improvements in the times as
you optimize the application.
Inspecting Performance
The React Profiler requires a development or profiling build of your
application to work. If you are still running the production build of
Performance Penguins with npm run serve in your terminal, exit it by
pressing Control-C. Then start the app in development mode by running npm
start. If it does not open automatically, open the Performance Penguins
home page at http://localhost:3000/.
Next, in the DevTools, make sure you have disabled network throttling in the
Network tab. If not, change Fast 3G back to No Throttling. Also, make
sure the box for Disable Cache is unchecked.
Now open the Profiler tab. Click the blue record button. In the website’s
header, click the Penguin Data link, wait for the page to load, then click the
record button again to stop profiling.
If this is your first time opening Penguin Data, the numbers toward the right
side of the Profiler tab’s menu bar will indicate two commits. A commit
occurs each time React writes the virtual DOM to the real DOM. Generally
speaking, it does this each time state changes and components re-render.
The first commit’s flamegraph looks like Figure 25.1, “First commit’s
flamegraph”:
In the header, the colored bars between the commit navigation arrows make
up a bar chart depicting the relative amount of time each commit required.
The selected commit is blue. Here, it is much shorter than the other commit,
because it took less time.
This commit includes the tasks for loading the screen while the dynamic
import resolves. (If you previously visited the Penguin Data page, you will
not see this commit, which is fine.)
View the flamegraph for the second commit by clicking the right arrow in the
menu bar. It looks like Figure 25.2, “Second commit’s flamegraph”:
This flamegraph shows that the Data component, highlighted in yellow, takes
up most of the time spent rendering. Though your specific results will be
different, for our test, Data took 361ms to render, over a third of the 835ms
for the page as a whole.
Now take a moment to familiarize yourself with the Penguin Data page by
playing with the filters and sorting options. Be patient! The data table loads
slowly, and it can look like the app freezes because the radio buttons and
dropdowns do not show the new values for a couple of seconds.
Let’s record the results of changing a filter. In the Profiler tab, click the blue
record button. Next, on the Penguin Data page, click the radio button to filter
the data for female penguins only. Wait for the table to update, then click the
record button in the Profiler tab to stop recording.
The result looks like Figure 25.3, “Profiling the filter change”:
The flamegraph shows details about how long each component took to
render. The timeline view shows a broader view of both the time it took to
render components and the time it took to write the changes out to the
browser.
Switch to the Profiler’s timeline tool by clicking its calendar icon in the menu
bar. Mouse over the different bars to see information about them
(Figure 25.4, “Timeline view”).
The first blue bar shows the time spent rendering, which you saw broken
down in the flamegraph. The purple bar to its right shows how long it took to
commit the change to the DOM. The next line shows a breakdown of how
long certain parts of the render took. The Data component took 120ms to
render. (The first blue bar, which shows the full rendering time, includes this
time.)
You will continue referring to the timeline view as you work on the rendering
performance as a whole.
Transitions
Though users of Performance Penguins want access to its important penguin
data, having the data controls lock up while the data loads is a bad user
experience. It would be better if the radio buttons, for example, updated
quickly to make the app feel more responsive.
React transitions allow your code to yield to important updates. React assigns
a lower priority to changes that happen within a transition than to other
updates. You will tell React that the filtered penguin data is low priority by
updating it in a transition. As a result, React will prioritize the updates to the
form fields, which are not in a transition, and keep the form responsive.
React uses state updates to track what is important versus what is a transition,
so the first step in using a transition is to store the filtered data in state.
useEffect(() => {
let filteredData = [...DataFile];
...
filteredData = filteredData.filter(({ age }) => age >= minAge);
filteredData = filteredData.filter(({ age }) => age <= maxAge);
const totalRows = filteredData.length;
filteredData.sort((a, b) => a[sort] - b[sort]);
setTotalRows(filteredData.length);
setRenderedData(filteredData);
}, [sexFilter, minAge, maxAge, sort]);
return (
...
<Table>
<thead>
...
</thead>
<tbody>
{filteredData.map(({
{renderedData.map(({
id, sex, height, weight, age,
...
You move the computation of the filtered data into a useEffect hook so that
the computation reruns only when the filters or sorting change.
A side effect of this change is that the update to the filter and the update to the
data are in separate render cycles. This already makes the radio buttons more
responsive.
Because of the amount of data on this page, live reload does not always work.
Save your file and refresh the page manually, then play with the filters to see
the changes.
useTransition from React will help fix both of these issues. It returns an array
with two values: The first is a boolean called isPending that indicates whether
a transition is waiting to be applied. The second is a function to start a new
transition.
return (
...
<Matches>
...
</Matches>
<Table>
<Table style={{ opacity: isPending ? 0.5 : 1 }}>
<thead>
...
On the other hand, you include setRenderedData inside the transition. This
means React will defer its execution while other, more significant updates take
place.
Finally, you take advantage of isPending to reduce the opacity of the table,
graying it out while updates are pending. This will help cue the user that an
update is taking place.
Refresh the page and play with the filters again to see the results.
Now let’s use the Profiler to quantify the results of adding a transition. Refresh
the page to reset the filters, and wait for the table to finish loading. Then click
the Profiler’s blue record button. On the website, click the radio button to filter
the data for female penguins only. Wait for the table to finish updating, then
click the record button again to stop profiling.
The flamegraph shows three commits. The first is the update to the filter state,
the second is the update to the total rows, and the third is the transition update
to the data.
Comparing three commits with the previous one commit is difficult in the
flamegraph. Switch to the timeline view, where you can see data from all three
commits at once (Figure 25.5, “Timeline with a transition”).
Take a look at the labels in gray. They are organized by the priority levels of
the updates they represent.
The first row (whose label is hidden behind the blue bar) is Sync. It represents
the highest-priority updates: the ones based on the state change that clicking
the Female radio button causes.
Though the purple commit bars after the first two sets of updates are very
short, React then spends 152ms committing the transition data.
The bottom of the timeline shows that Data renders three times, once for each
of the updates. The first render changes the selection of the radio button, and
the second changes the number next to Matching Rows. Though the amount of
time decreases, Data spends about 750ms rendering in total. Why does it take
so long to render those small changes?
Storing Rendered Content
Recall from Chapter 4, State that React maintains a virtual DOM, which it
computes when rendering components.
In this case, React re-renders all 20,000 rows of data in the virtual DOM with
every change. The commit to the real DOM is fast, because React can tell
that it has to update only one small thing. But React does a lot of work
computing the virtual DOM.
How can you stop the virtual DOM from re-rendering all the data every time
something changes? You can use a memo to save the work.
Edit components/Data.js to move the table rows into a memo with useMemo:
Now, iterating over all the data takes place inside useMemo. React will
recompute this only when renderedData changes, since it is a dependency of
useMemo.
In this case, you could have placed useMemo inline instead of creating the
rows variable. But it is often easier and more understandable to put your
hooks before the return statement rather than inside the returned JSX.
Play with the filters on the site a bit. They respond faster than before.
On the site, click the radio button to filter the data for female penguins
only.
When the table finishes updating, click the record button again to stop
profiling.
Look at the change to the beginning of the timeline (Figure 25.6, “Timeline
with useMemo”). (Hover your mouse over the blue bars to see the times.)
The first few times Data renders, it uses the memo, so the first blue bars at
the bottom are tiny. And now the total times for Sync (the top bar),
InputContinuous, and Default are all much shorter. Although the
Transition phase takes about the same amount of time as before, now the
other phases are much faster, so the page feels more responsive.
Storing a Rendered Component
Switch to the flamegraph to see what else you might be able to improve.
In the first and second commits, the bottom row shows a yellow component.
Hover your mouse over it to find out what it is (Figure 25.7, “Flamegraph
showing the Stats component”).
It is the Stats component, which renders at the top of the data page. Open
components/Stats.js in Visual Studio Code to see what the component
does.
This component computes the average age, height, and weight of the
penguins in the data set. The data set has 20,000 rows, so this is a fair amount
of work.
There are several ways to fix this. You can call useMemo on the averages to
cache them. Though the component will still re-render, the cached values will
speed it up. You can also use useEffect or useState to store the values. Or,
working with your project’s back-end team, you could have the server
precompute the averages, so the client never has to do it.
Another option is to cache the entire component with memo. This will make
the component behave like a class-based React component, re-rendering only
if props, state, or context changes.
Since nothing about this component will change (unless the data were to
change), the last option is the best one. Add a memo to components/Stats.js:
Repeat the experiment from above to get a new flamegraph so you can see
whether Stats still re-renders: Refresh the page and wait for the data to load.
Then start profiling, filter the data to show only female penguins, and stop
profiling once the table finishes updating (Figure 25.8, “The Stats
component still re-renders”).
Recall that memo re-renders if context, props, or state changes. Though Stats
does not use context or state, you can see in Data.js that it does have props:
return (
<DataDiv>
<Stats data={DataFile} setExactAge={setExactAge} />
...
The value for data is DataFile, which is a static import of penguin data.
Therefore, this value cannot change.
This changes the identity of the function. React compares props by identity,
so it thinks the function has changed each time its identity changes.
As you have seen before, the useCallback hook memoizes a function so that
it is not re-created and its identity does not change. Memoize setExactAge
with useCallback:
import {
useState, useEffect, useTransition, useMemo,
useState, useEffect, useTransition, useMemo, useCallback,
} from 'react';
...
function Data() {
...
const [isPending, startTransition] = useTransition();
const updateSex = (e) => setSexFilter(e.target.value);
const setExactAge = (age) => {
const setExactAge = useCallback((age) => {
setMinAge(age);
setMaxAge(age);
};
}, []);
useEffect(() => {
...
Recall that React guarantees the setter functions that useState returns will
always have the same identities, so you do not need to include them as
dependencies.
Repeat your profiling experiment once again to get a new flamegraph. The
Stats component no longer renders (Figure 25.9, “The Stats component
does not render”).
You might be wondering whether a better solution for the Penguin Data
page would have been to implement pagination in the data table so that only
50 rows would render at a time. The answer is yes – in this case, pagination
would make the commit phase much faster, and you might not even need to
use startTransition.
But in some cases, rendering 20,000 rows or 20,000 data points on a graph
is necessary. This is where startTransition comes in handy. It is also
useful for more computationally heavy rendering, such as highlighting all
instances of a word or computing values for a complex graph.
In this section, you learned about analyzing and optimizing the amount of
data your React application needs, which is especially important for clients
loading your app over slower connections. You learned about a new hook,
useTransition, which you can use to set a lower priority for certain
updates, and you used useMemo and memo to fix an especially slow
component. You also used useCallback again to preserve the identity of a
function.
Gold Challenge: Caching the Filtered Data
Users of the Penguin Data page are sorting data much more often than they
are filtering it. Split the current useEffect hook into two effects. The first
effect should handle data filtering, and the second effect should handle data
sorting.
Your goal is to lessen the performance impact for users sorting the data by
separating the sorting logic from the filtering logic.
Note: This is a hard one. If you are having trouble, chat with us on the
forums.
Chapter 26. Afterword
Congratulations! You are at the end of this book. Not everyone has the
discipline to do what you have done and learn what you have learned. Take
a quick moment to give yourself a pat on the back.
Your hard work has paid off: You are now a React developer.
Write code.
Right away. You will quickly forget what you have learned here if you do
not apply your knowledge. Contribute to a project or write a simple
application of your own. Whatever you do, waste no time: Write code.
Learn.
You have learned a little bit about a lot of things in this book. Did any of
them spark your imagination? Write some code to play around with your
favorite thing. Find and read more documentation about it – or an entire
book, if there is one. If you need suggestions, check out the React
Community Resource list at reactjs.org/community/support.xhtml.
Meet people.
Local meetups are good places to meet like-minded developers. Lots of top-
notch React developers are active on Twitter, such as Sophie Alpert
(@sophiebits) and Dan Abramov (@dan_abramov). And you can attend
conferences to meet other developers. (Maybe even us!)
If you enjoyed this book, check out the other Big Nerd Ranch Guides at
www.bignerdranch.com/books. We also have a broad selection of weeklong
courses for developers where we make it easy to learn a book’s worth of
stuff in only a week. And of course, if you just need someone to write great
code, we do contract programming too. For more info, go to
www.bignerdranch.com.
Thank You
Without readers like you, our work would not exist. Thank you for reading
our book!
Index
Symbols
accessibility
about, Accessibility Considerations
aria-label, Accessibility Considerations
component roles and, Conditionally Showing an Alert
form input focus and, Accessibility Considerations
form input labels and, Building the Form
action creators, Action Creators
afterAll blocks (Jest testing library), Mocking the Server
afterEach blocks (Jest testing library), Mocking the Server
App.js file
about, Customizing Your App
responsibilities, Home Component
application state (see state)
applications, React Programming
(see also React applications)
building, Building the Application
deploying, Deploying Your Application
favicons, Favicon
performance (see performance)
security, Cleaning Up useEffect
testing (see testing)
titles, Title
Array.filter (JavaScript), Removing Items from the Cart
Array.find (JavaScript), useState, Getting Additional Item Info
Array.map (JavaScript), Rendering Lists
Array.reduce (JavaScript), Displaying Information in the Header
as keyword, Router
async construct (JavaScript)
about, async/await
async/await vs then/catch, Comparing async/await and then/catch
asynchronous events, Promises, async/await
await (React Testing Library), Testing the Logged-In User’s Username
await construct (JavaScript)
about, async/await
async/await vs then/catch, Comparing async/await and then/catch
Axios library
DELETE requests, For the More Curious: Server Endpoints
GET requests, useEffect, For the More Curious: Server Endpoints
installing, Adding an HTTP request library
POST requests, For the More Curious: Server Endpoints, Submitting a
Form, Logout Button
promises, Promises-How promises work, async/await-Comparing
async/await and then/catch
PUT requests, For the More Curious: Server Endpoints
C
catch construct (JavaScript)
about, Promises
then/catch vs async/await, Comparing async/await and then/catch
children prop
about, Alert
passing child elements inline vs, Component Composition with Props
Chrome Developer Tools
Application tab, Local Storage, For the More Curious: Login Cookie
Components tab (React Developer Tools), React Developer Tools
Console tab, Keys
device toolbar, Responsive Design
Elements tab, The Chrome Developer Tools
HTTP requests and, Confirming the Network Request
inspecting website elements, Large Images
modeling network conditions, Preventing Inadvertent Submissions
Network tab, Confirming the Network Request
opening, The Chrome Developer Tools
Profiler tab (React Developer Tools), React Profiler
(see also React Profiler)
React Developer Tools, React Developer Tools
viewing server responses, Confirming the Network Request
chunks, webpack, Lazy Loading
clearTimeout (JavaScript), Using setTimeout with useRef
code splitting, Lazy Loading
command line, Crash Course in the Command Line-Quitting a program
commits, React render cycle, Inspecting Performance
components, React Programming
(see also React Router library)
about, App Organization
as functions, Header Component
as props, Component Composition with Props
children prop, Alert
(see also children prop)
class-based, useState, For the More Curious: Components – Functions
vs Classes-For the More Curious: Components – Functions vs Classes,
For the More Curious: useEffect vs Lifecycle Methods
composition, Component Composition
conditional rendering, Conditional Rendering
controlled vs uncontrolled, Value from Inputs
file conventions, Header Component
functional vs class-based, For the More Curious: Components –
Functions vs Classes
named functional components vs arrow functions, Why name
components?
props, Props-Object destructuring
(see also props)
re-renders due to context changes, Context Changes
re-renders due to parent re-rendering, useState, React Profiler
re-renders due to props changes, Context Changes
re-renders due to state changes, What Is State?
responsibilities, vs App.js, Home Component
reusing with props, Props
roles, Conditionally Showing an Alert, Testing Navigation
state (see state)
stylesheets for, Styling the header
viewing in the React Developer Tools, React Developer Tools
conditional rendering, Conditional Rendering
console (Chrome Developer Tools), Keys
context
about, Context, Avoiding Prop Drilling
component composition vs, Avoiding Prop Drilling
Consumer component, Passing currentUser with Context
creating, Passing currentUser with Context
Provider component, Passing currentUser with Context
React render cycle and, useMemo, Context Changes
subscribing to, useContext
useContext, useContext
context (MSW package), Mocking the Server
cookies, For the More Curious: Login Cookie
Create React App, React Programming
(see also React applications)
about, Create React App
development server, Running the Development Server-Running the
Development Server
ESLint rules and, Linting Rules
image inlining, For the More Curious: Inlining Images
React.StrictMode, src directory, Confirming the Network Request
testing libraries, Testing with Jest and the React Testing Library
createContext, Passing currentUser with Context
CSS, React Programming
(see also styles)
global scope, Header Component, Style Scoping
:hover pseudo-class, Hover Effect
media queries, Responsive Design
selectors, Styles, Style Scoping
transform property, Hover Effect
transition property, Animating the hover effect
Cypress, React Programming
(see also Cypress Testing Library)
about, End-to-End Testing
chainers, Testing the Login Flow
creating custom commands, Factoring Out Custom Commands
describe blocks, Testing the Login Flow
ESLint and, Installing and Configuring Cypress, Setting up ESLint for
Cypress-Setting up ESLint for Cypress
file names and locations, Setting up ESLint for Cypress, Testing the
Login Flow
installing, Installing and Configuring Cypress
invisible elements, Invisible Elements
it blocks, Testing the Login Flow
onBeforeLoad, Testing errors in the checkout flow
retryable commands, Testing the Login Flow
simulating user events, Testing the Login Flow
specs, Configuring scripts
starting, Configuring scripts
stub, Testing errors in the checkout flow
testing server integration, Interacting with the server
visit, Testing the Login Flow, Testing errors in the checkout flow
windows, Testing errors in the checkout flow
Cypress Testing Library
about, End-to-End Testing
base URL, Setting the base URL
findAllBy, Testing the Login Flow
findBy, Testing the Login Flow, Testing the Checkout Flow
importing commands, Importing commands
installing, Installing and Configuring Cypress
JavaScript
!! (double NOT operator), The && operator, Boolean Type
Conversion
&& (logical AND operator), The && operator
... (spread syntax), The Reducer Function
?? (nullish coalescing operator), The ?? operator
|| (logical OR operator), The || operator
Array.filter, Removing Items from the Cart
Array.find, useState, Getting Additional Item Info
Array.map, Rendering Lists
Array.reduce, Displaying Information in the Header
async, async/await, Comparing async/await and then/catch
await, async/await, Comparing async/await and then/catch
block comments in JSX, Creating the Details Page and Route
catch, Promises, Comparing async/await and then/catch
clearTimeout, Using setTimeout with useRef
focus, Accessing DOM Elements
if, if Statements
localStorage.setItem, Local Storage
preventDefault, onSubmit
setTimeout, Using setTimeout with useRef
ternary conditionals, Inline Logical Operators
then, Promises, Comparing async/await and then/catch
toFixed, Getting Additional Item Info
truthy and falsy values, Inline Logical Operators
Jest testing library
about, Testing with Jest and the React Testing Library
afterAll blocks, Mocking the Server
afterEach blocks, Mocking the Server
beforeAll blocks, Mocking the Server
beforeEach blocks, Mocking console.error
describe blocks, Unit Testing
expect statements, Unit Testing
fake DOM, Testing Thumbnail
file names and locations, Housekeeping
for integration tests, Integration Testing
for unit tests, Unit Testing
invisible elements, Invisible Elements
it blocks, Unit Testing
matchers, Unit Testing, Testing Home
navigation testing, Testing Navigation
render, Testing Thumbnail
running tests, Unit Testing
screen, Testing Thumbnail
spyOn, Mocking console.error
toEqual matcher, Unit Testing
toHaveLength matcher, Testing Home
user-event library and, Testing Navigation
watch usage mode, Unit Testing
writing tests, Unit Testing
jest-dom library
about, Testing with Jest and the React Testing Library
matchers, Testing the Add to Cart button
toBeVisible matcher, Testing checkout
toHaveTextContent matcher, Testing the Add to Cart button
JSX
about, Customizing Your App, JSX
buttons, Adding Elements, Building the Form
className attributes, Styles
expressions, Props
fragments, Fragments
htmlFor attribute, Building the Form
inputMode attribute, Building the Form
JavaScript block comments in, Creating the Details Page and Route
JavaScript inside, Rendering Lists
.js and .jsx file extensions, Overriding ESLint rules
maxLength attribute, Building the Form
type attribute, Building the Form
whitespace in text output, Running ESLint
naming conventions
component files, Header Component
components, Header Component
events, User Events
files, For the More Curious: Alternate file-naming conventions
hooks, useState
inline style properties, Reusing Alert
props parameter, Props
state update functions, useState
networking
asynchronous events, Promises, async/await
(see also promises)
HTTP requests, Adding an HTTP request library, Submitting a Form
(see also Axios library)
modeling network conditions in the Chrome Developer Tools,
Preventing Inadvertent Submissions
preventing accidental submissions, Preventing Inadvertent
Submissions
proxies, Creating a proxy
viewing network requests in the Chrome Developer Tools, Confirming
the Network Request, Submitting a Form
Node.js, installing, Installing Node.js
node_modules directory, Project metadata and dependencies
npm install command vs manual package installation, Installing and
configuring Airbnb’s ESLint
npx, For the More Curious: npx
nullish coalescing operator (??) (JavaScript), The ?? operator
useCallback
about, useCallback
compared with memo and useMemo, Comparing useCallback,
useMemo, and memo
useContext, useContext
(see also context)
useEffect
about, useEffect
cleanup function, Cleaning Up useEffect
compared with lifecycle methods, For the More Curious: useEffect vs
Lifecycle Methods
synchronous functions and, Getting the Logged-In User
useMemo
compared with memo and useCallback, Comparing useCallback,
useMemo, and memo
to prevent unnecessary calculations, Expensive Calculations
to prevent unnecessary identity changes, useMemo
useNavigate (React Router library), useNavigate
useParams (React Router library), useParams
user-event library
about, Testing with Jest and the React Testing Library
simulating clicks, Testing Navigation
versions, Housekeeping
useReducer
about, useReducer
action creators, Action Creators
action types, The Reducer Function
dispatch function, Implementing useReducer
reducer functions, The Reducer Function
returning a copy of state, The Reducer Function
scope, Viewing the Cart’s Contents
switch cases in reducer functions, The Reducer Function
useState vs, useReducer vs useState
useRef
about, Local Storage and useRef
for accessing DOM elements, Accessing DOM Elements
for debouncing, Using setTimeout with useRef
useState vs, useRef vs useState
users
getting the logged-in user, Getting the Logged-In User
logging in, Login Component
logging out, Logout Button
restricting access based on authentication, Orders Link, Restricting
Access with a Custom Hook-Cleaning Up useEffect
useState
about, useState
useReducer vs, useReducer vs useState
useRef vs, useRef vs useState
useTransition, Transitions
V
visit (Cypress), Testing the Login Flow, Testing errors in the checkout flow
Visual Studio Code
creating files, Header Component
creating folders, Header Component
ESLint extension, Installing the ESLint Extension in Visual Studio
Code-For the More Curious: Shortcuts to Running ESLint in Visual
Studio Code
explorer pane, Navigating Your App
finding all instances of an element, Navigating from Thumbnails to
Details
indenting multiple lines, Adding SelectedItem
installing, Installing Visual Studio Code
new line character settings, Whitespace issues
opening a project, Navigating Your App
tab size settings, Whitespace issues