0% found this document useful (0 votes)
38 views105 pages

Juntao Qiu - Advanced Data Fetching Patterns in React - 2024

Uploaded by

lochinbek.b
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
0% found this document useful (0 votes)
38 views105 pages

Juntao Qiu - Advanced Data Fetching Patterns in React - 2024

Uploaded by

lochinbek.b
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
Download as pdf or txt
Download as pdf or txt
You are on page 1/ 105

Advanced Data Fetching

Patterns in React
Fast, User-Friendly Data Fetching for
Developers

Juntao Qiu
This book is for sale at
http://leanpub.com/react-data-fetching-patterns

This version was published on 2024-01-27

This is a Leanpub book. Leanpub empowers authors and


publishers with the Lean Publishing process. Lean Publishing is
the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have
the right book and build traction once you do.

© 2024 Juntao Qiu


Contents

Preface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

Chapter 1: Introduction . . . . . . . . . . . . . . . . . . . . . 2
Setting up the environment . . . . . . . . . . . . . . . . . 4
Setting up the backend service . . . . . . . . . . . . . . . . 7

Chapter 2: Basics of Data Fetching in React . . . . . . . . 9


Adding loading and error handling . . . . . . . . . . . . . 13
Implementing the User’s Friends List . . . . . . . . . . . . 16

Chapter 3: Fetching Resources in Parallel . . . . . . . . . . 19


Sending Requests in Parallel . . . . . . . . . . . . . . . . . 22
Request Dependency . . . . . . . . . . . . . . . . . . . . . 26

Chapter 4: Optimizing Friend List Interactions . . . . . . . 30


Install and config NextUI . . . . . . . . . . . . . . . . . . . 31
Implementing a Popover Component . . . . . . . . . . . . 33
Defining a Trigger Component . . . . . . . . . . . . . . . 34
Implementing UserDetailCard Component (Fetching Data) 35

Chapter 5: Leveraging Lazy Load and Suspense in React . 40


Introducing Lazy Loading . . . . . . . . . . . . . . . . . . 41
Implementing UserDetailCard with lazy loading . . . . . 43
The potential issue . . . . . . . . . . . . . . . . . . . . . . 49

Chapter 6: Prefetching Techniques in React Applications 51


Introducing SWR . . . . . . . . . . . . . . . . . . . . . . . 52
Implementing SWR for Preloading . . . . . . . . . . . . . 53
Integrating preload with SWR . . . . . . . . . . . . . . . . 53

Chapter 7: Introducing Server Side Rendering . . . . . . . 59


Introducing Next.js . . . . . . . . . . . . . . . . . . . . . . 60
React Server Components . . . . . . . . . . . . . . . . . . 65

Chapter 8: Introducing Static Site Generation . . . . . . . 67


Mixing Server-Side Rendering and Static Site Generation 69

Chapter 9: Optimizing User Experience with Skeleton


Loading in React . . . . . . . . . . . . . . . . . . . . . . . 72

Chapter 10: The New Suspense API in React . . . . . . . . 77


Re-Introducing Suspense & Fallback . . . . . . . . . . . . 78
Using skeletons in different layers . . . . . . . . . . . . . 82
Streaming in Next.js . . . . . . . . . . . . . . . . . . . . . 84
Optimizing UI by Grouping Related Data Components . 85

Chapter 11: Lazy Load, Dynamic Import, and Preload in


Next.js . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Dynamic Load in Next.js . . . . . . . . . . . . . . . . . . . 89
Implementing the UserDetailCard . . . . . . . . . . . . . . 91
Preload in Next.js . . . . . . . . . . . . . . . . . . . . . . . 93
Client Component Strategy . . . . . . . . . . . . . . . . . 97
Preface
As your React applications grow richer with API integrations, do
you find them increasingly bogged down and sluggish? You’re
wrestling with a common dilemma in the modern web develop-
ment landscape: the more features and data connections you add,
the more complex and slower your app becomes. Add to this
the often baffling world of asynchronous programming, where
debugging and troubleshooting can feel like navigating a labyrinth
in the dark.
And it doesn’t stop there. The React ecosystem is rapidly evolving,
bombarding you with a flurry of new terms and concepts. React
Server Components, SSR (Server-Side Rendering), Suspense
API – these aren’t just fancy buzzwords; they’re game-changers in
how we think about building and optimizing our applications. But
understanding and implementing them can feel overwhelming.
Embark on this journey with me, and together we’ll take these
intricate concepts and break them down into digestible, actionable
insights. From unraveling the challenges of parallel requests and
mastering lazy loading to demystifying SSR and exploring the
cutting-edge realms of Server Components and Suspense, I’ve got
you covered. You’ll not only learn how to keep your applications
lightning-fast but also discover strategies for efficient debugging
and problem-solving in the asynchronous realm of React.
Join me, and transform the way you develop, optimize, and debug
your React applications. It’s time to turn those pain points into your
strongest assets.
Chapter 1: Introduction
Kicking off the tutorial ‘Advanced Network Patterns in React’,
this chapter sets the stage for exploring diverse network re-
quest patterns in React applications. It establishes the foun-
dational knowledge needed for handling complex network
scenarios in frontend development.

In this chapter, you will learn the following content:

• Setting up the development environment with Vite and Tail-


wind CSS.
• Ensuring all necessary tools and configurations are in place
for the tutorial.
• Introduction to the mock server for backend simulation.

This tutorial is designed to explore a range of patterns for executing


network requests in React applications. Although the focus is on
React, the principles and challenges discussed are relevant to other
frontend libraries and frameworks.
Starting with a basic user profile, the tutorial progressively intro-
duces more complex scenarios. These scenarios are intended to
illuminate common problems and solutions encountered in large-
scale applications, with a primary aim of demystifying the often-
overlooked performance pitfalls in frontend development.
While React serves as the primary example, the patterns and
strategies discussed are broadly applicable across various frontend
technologies. They offer universal insights that can be valuable in
diverse development contexts.
Chapter 1: Introduction 3

The tutorial assumes you have a foundational understanding of


React, including familiarity with JSX and common hooks such as
useState and useEffect.

Key learning outcomes of this tutorial include:

• Gaining a deep understanding of the challenges inherent in


network programming and why it can be difficult to get right.
• Unraveling and demystifying the most commonly misunder-
stood yet widely used patterns.
• Exploring ways to make asynchronous service calls more
manageable and less error-prone.
• Investigating alternative approaches for enhancing user ex-
perience.
• Learning how to apply different strategies in both frontend
and backend development.
• Looking ahead to the future of server-side work and its
implications for frontend development.

This tutorial aims to equip you with the knowledge and tools to
navigate the complexities of network requests in React and other
frontend frameworks, enhancing both your understanding and
practical skills.
The page we’re going to build is a Home page in an imaginary social
media website. It doesn’t do much but showing a user their home
page when then log in.
Chapter 1: Introduction 4

Figure 1. The home page we will build in the tutorial

Setting up the environment


We’re going to use vite as the scaffolding tool to generate the
structure of the application, use the following command to create
a React with TypeScript enabled.

1 npm create vite@latest react-network-advanced -- --templa\


2 te react-ts
3 cd react-network-advanced

Let’s clean up the generated template file, open up the src/App.tsx,


and put the following code:
Chapter 1: Introduction 5

1 function App() {
2 return (
3 <div className="max-w-3xl m-auto my-4 text-slate-80\
4 0">
5 <h1 className="text-4xl py-4 mb-4 tracking-wider \
6 font-bold">Profile</h1>
7 </div>
8 );
9 }
10
11 export default App;

We are going to use Tailwind Css for styling in this tutorial.

Tailwind is a utility-first CSS framework packed with classes


like flex, pt-4, text-center and rotate-90 that can be composed
to build any design, directly in your markup.
Utility-first CSS frameworks provide a comprehensive set of
CSS utility classes for common styling tasks. Instead of writing
custom CSS, developers can construct designs by combining
these utility classes directly in their markup. This approach
promotes rapid UI development, consistency across pages, and
can lead to more maintainable codebases.

To install Tailwind, go to the react-network-advanced folder we


created above, and execute the following command in Terminal (if
you’re on Mac OS)

1 npm install -D tailwindcss postcss autoprefixer


2 npx tailwindcss init -p

And then you will need to config Tailwind to allow it scan


and all tsx files under src folder.
index.html
Chapter 1: Introduction 6

Figure 2. tailwind.config.js

1 /** @type {import('tailwindcss').Config} */


2 export default {
3 content: [
4 "./index.html",
5 "./src/**/*.{ts,tsx}",
6 ],
7 theme: {
8 extend: {},
9 },
10 plugins: [],
11 }

Lastly, you will need to use @tailwind directives in src/index.css


to actually enable it:
Figure 3. src/index.css

1 @tailwind base;
2 @tailwind components;
3 @tailwind utilities;

I prefer to make the background a bit gray, so I’ll add the following
line in src/index.css
Figure 4. src/index.css

1 body {
2 background-color: #fefefe;
3 }

With these changes in place, let’s launch the application now in


command line:
Chapter 1: Introduction 7

1 npm run dev

It by default will launch your React application on


“http://localhost:5173/”, And in your browser, you should be
able to see the text Profile

Figure 5. Check Vite and Tailwind CSS are working together

Setting up the backend service


We are going to call some API endpoints in the course of
the tutorial, I have published them into a Github repo1 , go
ahead and clone the repo to your local (assume it’s in folder:
mock-server-network-react-tutorial.

Run the following command to launch the mock server:

1 cd mock-server-network-react-tutorial
2 yarn start

You would be able to see something like this in your console:

1 https://github.com/abruzzi/mock-server-network-react-tutorial
Chapter 1: Introduction 8

1 yarn run v1.22.19


2 $ node index.js
3 Mock server listening at http://localhost:1573

And if you try to access the one of the following API endpoint:

1 curl http://localhost:1573/users/u1

Or if you prefer, you could use jq to format the output, which is a


bit easier to read (curl http://localhost:1573/users/u1 | jq .).
And you should be able to see response like the following:

1 {
2 "id": "u1",
3 "name": "Juntao Qiu",
4 "bio": "Developer, Educator, Author",
5 "interests": [
6 "Technology",
7 "Outdoors",
8 "Travel"
9 ]
10 }

This introductory chapter equips learners with the essential setup


and context for tackling advanced network patterns in React,
paving the way for more complex concepts and practical appli-
cations in subsequent chapters. The next chapter will dive into
using useEffect for building a basic profile page, demonstrating
sequential network requests.
Chapter 2: Basics of Data
Fetching in React
This chapter dives into the essentials of data fetching in React
applications, starting from a simple user profile page. It
highlights the initial steps of making API calls, dealing with
network delays, and managing state with React’s useEffect
hook.

In this chapter, you will learn the following content:

• Understanding and implementing basic data fetching using


useEffect in React.
• Exploring the impact of network delays on frontend perfor-
mance.
• Practical implementation of a user profile and friends list
component with API integration.

Imagine a simple React application: a profile page where a logged-


in user can view their profile. To achieve this, we need to fetch the
user’s information from an API using their ID.
The API endpoint we’ll use returns basic user information. It’s
designed to include a delay, allowing us to later examine how slow
API responses can impact frontend performance.
Consider this API call:
Chapter 2: Basics of Data Fetching in React 10

1 curl http://localhost:1573/users/u1

And the expected response:

1 {
2 "id": "u1",
3 "name": "Juntao Qiu",
4 "bio": "Developer, Educator, Author",
5 "interests": [
6 "Technology",
7 "Outdoors",
8 "Travel"
9 ]
10 }

In a typical React setup, we would handle this data fetching within a


useEffect call. React triggers this effect after completing the initial
render.
Here’s how you might implement this in src/profile.tsx:
Figure 6. src/profile.tsx

1 const Profile = ({ id }: { id: string }) => {


2 const [user, setUser] = useState<User | undefined>();
3
4 useEffect(() => {
5 const fetchUser = async () => {
6 const data = await get<User>(`/users/${id}`);
7 setUser(data);
8 };
9
10 fetchUser();
11 }, [id]);
12
13 return (
14 <div>
Chapter 2: Basics of Data Fetching in React 11

15 {user && user.name}


16 </div>
17 );
18 };

The get function is a straightforward wrapper around the native


fetch. You can replace it with axios.get or any other preferred
method.
Here’s the utility function in utils.ts:
Figure 7. utils.ts

1 const baseurl = "http://localhost:1573";


2
3 async function get<T>(url: string): Promise<T> {
4 const response = await fetch(`${baseurl}${url}`);
5
6 if (!response.ok) {
7 throw new Error("Network response was not ok");
8 }
9
10 return await response.json() as Promise<T>;
11 }
12
13 export { get };

To render the Profile component, update App.tsx as follows:


Chapter 2: Basics of Data Fetching in React 12

Figure 8. App.tsx
1 import { Profile } from "./src/profile.tsx";
2
3 function App() {
4 return (
5 <div>
6 <h1>Profile</h1>
7 <div>
8 <Profile id="u1" />
9 </div>
10 </div>
11 );
12 }
13
14 export default App;

Visualizing the rendering sequence over time, it would look some-


thing like this diagram:

Figure 9. Fetch and then render

When a user accesses a React application, a typical flow begins


with the browser downloading the initial HTML. As it parses the
Chapter 2: Basics of Data Fetching in React 13

HTML, it encounters links to resources like JS and CSS. This process


involves downloading, parsing, and executing JS bundles, building
the CSSOM, and so on.

Note: HTML parsing is usually done in a streaming manner,


meaning it starts as soon as some bytes are received rather than
waiting for the entire HTML to download. For simplicity, our
illustration assumes the entire HTML is downloaded before
DOM construction. More details can be found in Rendering on
the Web1 , Client-side rendering of HTML and interactivity2 ,
and Populating the page: how browsers work3 .

As JavaScript executes, React begins rendering and manipulating


the DOM, then triggers a network request through useEffect. It
waits until data returns from the server before re-rendering with
the new data.

Adding loading and error handling


To enhance user experience, we can introduce a Spinner component
during data loading and an Error component for handling issues
like unresponsive backends or nonexistent users.
With these additions, the Profile component now includes addi-
tional states:

1 https://web.dev/articles/rendering-on-the-web
2 https://web.dev/articles/client-side-rendering-of-html-and-interactivity
3 https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work
Chapter 2: Basics of Data Fetching in React 14

Figure 10. src/profile.tsx

1 const Profile = ({ id }: { id: string }) => {


2 const [loading, setLoading] = useState<boolean>(false);
3 const [error, setError] = useState<Error | undefined>();
4
5 const [user, setUser] = useState<User | undefined>();
6
7 useEffect(() => {
8 const fetchUser = async () => {
9 try {
10 setLoading(true);
11 const data = await get<User>(`/users/${id}`);
12 setUser(data);
13 } catch (e) {
14 setError(e as Error);
15 } finally {
16 setLoading(false);
17 }
18 };
19
20 fetchUser();
21 }, [id]);
22
23 if (loading) {
24 return <div>Loading...</div>;
25 }
26
27 if (error) {
28 return <div>Something went wrong...</div>;
29 }
30
31 return (
32 <>
33 {user && user.name}
34 </>
Chapter 2: Basics of Data Fetching in React 15

35 );
36 };

This structure should be familiar if you’ve worked with React


before.
Next, let’s add more than just the username. We’ll create an About
component to display user information, adding simple styles for
visual appeal.

For brevity, I’m omitting Tailwind CSS from the code snippets.
You can view the full styled components in the corresponding
code repository.

Figure 11. src/about.tsx

1 const About = ({ user }: { user: User }) => {


2 return (
3 <div>
4 <div>
5 <img
6 src={user.avatar}
7 alt={`User ${user.name} Avatar`}
8 />
9 </div>
10 <div>
11 <div>{user.name}</div>
12 <p>{user.bio}</p>
13 </div>
14 </div>
15 );
16 };
Chapter 2: Basics of Data Fetching in React 16

When data is correctly fetched, it renders as shown:

Figure 12. User basic information fetched and rendered

We now have a basic Profile page, retrieving data from a backend


API intentionally delayed by 1.5 seconds.

Implementing the User’s Friends List


Consider the user’s friends list, typically stored in a separate
table and accessed via a different API endpoint. For example,
/users/<id>/friends. We’ll fetch this data in a new component,
Friends.

Figure 13. src/friends.tsx

1 const Friends = ({ id }: { id: string }) => {


2 const [loading, setLoading] = useState<boolean>(false);
3 const [users, setUsers] = useState<User[]>([]);
4
5 useEffect(() => {
6 const fetchFriends = async () => {
7 setLoading(true);
8 const data = await get<User>(`/users/${id}/friends`\
Chapter 2: Basics of Data Fetching in React 17

9 );
10 setLoading(false);
11 setUsers(data);
12 };
13
14 fetchFriends();
15 }, [id]);
16
17 if(loading) {
18 return <div>Loading...</div>
19 }
20
21 return (
22 <div>
23 <h2>Friends</h2>
24 <div>
25 {users.map((user) => (
26 <div>
27 <img
28 src={`https://i.pravatar.cc/150?u=${user.id\
29 }`}
30 alt={`User ${user.name} avatar`}
31 />
32 <span>{user.name}</span>
33 </div>
34 ))}
35 </div>
36 </div>
37 );
38 };
39
40 export { Friends };

The structure of the Friends component mirrors that of Profile:


managing state, fetching data in useEffect, and rendering based
Chapter 2: Basics of Data Fetching in React 18

on loading, error, and successful data retrieval states.


We can incorporate Friends into the Profile component like any
regular React component:
Figure 14. src/profile.tsx

1 const Profile = () => {


2 //...
3
4 return (
5 <>
6 {user && <About user={user} />}
7 <Friends id={id} />
8 </>
9 );
10 }

At first glance, this implementation seems fine. However, if you


consider the time taken for each API call, you might spot a potential
issue. What if the /friends API also takes 1.5 seconds to respond?
The total time to display the full page would be 3 seconds.
This chapter lays the groundwork for mastering network requests
in React. It provides a practical example of fetching and rendering
data, setting the stage for more advanced patterns and perfor-
mance considerations in subsequent chapters. The next chapter
will further explore complex data fetching scenarios, addressing
performance challenges in frontend applications.
Chapter 3: Fetching
Resources in Parallel
Chapter 3 tackles the challenge of optimizing network requests
in React applications. It focuses on implementing parallel
data fetching strategies to minimize the impact of the network
waterfall effect, enhancing application performance and user
experience.

In this chapter, you will learn the following content:

• Understanding and mitigating the request waterfall effect.


• Implementing parallel requests for efficiency.
• Handling dependencies in network requests.

The first issue we encounter is with the rendering order. Initially,


in the Profile component, useEffect triggers a network request.
However, since data takes 1.5 seconds to return, we display a
loading indicator in the meantime.
Once the data arrives, we render the About section, and a similar
process occurs in the Friends component. Here, useEffect initiates
another network request, waiting for the data to return.
Visualizing the request timeline, it looks like this:
Chapter 3: Fetching Resources in Parallel 20

Figure 15. The request waterfall issue

The process involves three renderings. After the first render, the
page displays a loading... message while initiating the /users/u1
request. When the server responds, the About section is displayed.
As Friends renders, lacking available data, it shows a loading...
message in its section and sends out the /users/u1/friends request.
Upon receiving this data, the third rendering occurs.
Chapter 3: Fetching Resources in Parallel 21

Figure 16. The component tree of About + Friends

This sequence might be obvious in our current setup, but consider


more complex scenarios. Imagine the Friends component nested
deeper in the component tree or used in different pages or sections.
In such cases, identifying the problem by statically reading the code
becomes challenging.
The situation worsens with more nested components following the
same useEffect + loading + error pattern, potentially leading to
cumulative performance issues:
Chapter 3: Fetching Resources in Parallel 22

Figure 17. Request waterfall could be even worse

Over time, as the component tree grows, the page becomes increas-
ingly slower.
However, one might wonder if initiating data fetching simultane-
ously could mitigate this wait time.

Sending Requests in Parallel


We can address this issue by sending parallel requests. In the
Profile component, we can start both requests simultaneously
using Promise.all, passing the fetched friends list to the Friends
component:
Chapter 3: Fetching Resources in Parallel 23

Figure 18. src/profile.tsx

1 const Profile = ({ id }: { id: string }) => {


2 //...
3 const [user, setUser] = useState<User | undefined>();
4 const [friends, setFriends] = useState<User[]>([]);
5
6 useEffect(() => {
7 const fetchUserAndFriends = async () => {
8 try {
9 setLoading(true);
10
11 const [user, friends] = await Promise.all([
12 get<User>(`/users/${id}`),
13 get<User[]>(`/users/${id}/friends`),
14 ]);
15
16 setUser(user);
17 setFriends(friends);
18 } catch (e) {
19 setError(e as Error);
20 } finally {
21 setLoading(false);
22 }
23 };
24
25 fetchUserAndFriends();
26 }, [id]);
27
28 //...
29 };
30
31 export { Profile };
Chapter 3: Fetching Resources in Parallel 24

The Promise.all() static method accepts an iterable of


promises, returning a single Promise. This promise resolves
when all input promises fulfill (including for an empty
iterable), resulting in an array of fulfillment values. If any
input promises reject, the returned promise rejects with the
first rejection reason.

Consequently, we modify Friends into a presentational component,


responding only to the passed users list, rather than making its own
requests:
Figure 19. src/friends.tsx
1 const Friends = ({ users }: { users: User[] }) => {
2 return (
3 <div>
4 <h2>Friends</h2>
5 <div>
6 {users.map((user) => (
7 <div>
8 <img
9 src={`https://i.pravatar.cc/150?u=${user.id\
10 }`}
11 alt={`User ${user.name} avatar`}
12 />
13 <span>{user.name}</span>
14 </div>
15 ))}
16 </div>
17 </div>
18 );
19 };
20
21 export { Friends };
Chapter 3: Fetching Resources in Parallel 25

Now, the total wait time is reduced to max(1.5, 1.5) = 1.5 seconds,
a significant improvement:

Figure 20. Send requests in parallel

The only remaining issue is the potential wait for the slower request
in extreme cases. We’ll accept this limitation for now and explore
solutions in subsequent chapters.
Chapter 3: Fetching Resources in Parallel 26

Figure 21. The only async component now is Profile

Request Dependency
Parallel requests expedite the loading of independent data. How-
ever, some requests depend on others. For example, we might need
to fetch user information first and use the interests array from
the response to retrieve recommended feeds for the user. This
sequential dependency necessitates a return to the initial approach.
In the Feeds component, we define loading, error, and data states,
and useEffect initiates network fetching after the initial render:
Chapter 3: Fetching Resources in Parallel 27

Figure 22. src/feeds.tsx

1 const Feeds = ({ category }: { category: string }) => {


2 const [loading, setLoading] = useState<boolean>(false);
3 const [feeds, setFeeds] = useState<Feed[]>([]);
4
5 useEffect(() => {
6 const fetchFeeds = async () => {
7 setLoading(true);
8 const data = await get(`/articles/${category}`);
9
10 setLoading(false);
11 setFeeds(data);
12 };
13
14 fetchFeeds();
15 }, [category]);
16
17 if (loading) {
18 return <div>Loading...</div>;
19 }
20
21 return (
22 <div>
23 <h2>Your Feeds</h2>
24 <div>
25 {feeds.map((feed) => (
26 <>
27 <h3>{feed.title}</h3>
28 <p>{feed.description}</p>
29 </>
30 ))}
31 </div>
32 </div>
33 );
34 };
Chapter 3: Fetching Resources in Parallel 28

In the Profile component, we include Feeds as follows:


Figure 23. src/profile.tsx

1 return (
2 <>
3 {user && <About user={user} />}
4 <Friends users={friends} />
5 {user && <Feeds category={user.interests[0]} />}
6 </>
7 );

Initially, About and Friends load, and as soon as the user data is
available, we use interests[0] to fetch feeds, potentially taking
another second. The overall wait time amounts to max(1.5, 1.5)
+ 1 = 2.5 seconds.

Figure 24. The UI after all components are rendered


Chapter 3: Fetching Resources in Parallel 29

This approach combines parallel and sequential requests, yielding


better performance than the initial method.

Figure 25. The mix of parallel and sequential requests

The feeds request must wait for the completion of the previous
two requests, displaying a large spinner in the interim. While
this solution is functional, we must consider the runtime data
requirements for each specific user.
By mastering parallel requests and managing dependencies in
network calls, this chapter sets the foundation for building faster
and more responsive React applications. Join us as we continue to
navigate the intricate world of advanced network patterns in React.
In the next chapter, we explore further optimization strategies and
delve into more complex scenarios of network fetching in React.
Chapter 4: Optimizing
Friend List Interactions
This chapter focuses on enhancing the user experience in React
applications by implementing a detailed user profile popover.
It explores the integration of external UI libraries like NextUI
for building interactive features and discusses efficient data
fetching strategies.

In this chapter, you will learn the following content:

• Leveraging NextUI for dynamic UI components.


• Efficient data fetching for enhanced features.
• Balancing performance with interactive design.

Let’s delve further into typical frontend application scenarios,


uncovering new patterns and potential avenues for performance
improvement.
Consider enhancing the current Profile page. Suppose a user clicks
on a friend’s avatar, and we display a popover with additional
details fetched from a /users/1/details endpoint. This feature,
common in platforms like Twitter or LinkedIn, adds depth to user
interaction.
Chapter 4: Optimizing Friend List Interactions 31

Figure 26. User Detail Popover

To maintain focus on our main topic, I’ll skip the detailed imple-
mentation of the popover itself. Instead, we’ll utilize components
from nextui for the popover behavior and UserDetailCard.

NextUI is a React UI library built on top of Tailwind CSS and


React Aria, offering beautiful and accessible user interfaces.
Despite its name similarity and website design, it’s an inde-
pendent community project and is not affiliated with Vercel
or Next.js.

Install and config NextUI


Firstly, let’s install NextUI into our project:
Chapter 4: Optimizing Friend List Interactions 32

1 yarn add @nextui-org/react framer-motion

And the we will need to edit tailwind.config.cjs


Figure 27. tailwind.config.cjs

1 const {nextui} = require("@nextui-org/react");


2
3 /** @type {import('tailwindcss').Config} */
4 module.exports = {
5 content: [
6 "./index.html",
7 "./src/**/*.{jsx,tsx}",
8 "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,js\
9 x,tsx}",
10 ],
11 theme: {
12 extend: {},
13 },
14 darkMode: "class",
15 plugins: [nextui()],
16 }

And finally we’ll need to wrap the Application with a


NextUIProvider:

1 import { NextUIProvider } from "@nextui-org/react";


2
3 function App() {
4 return (
5 <NextUIProvider>
6 <div>
7 <h1>Profile</h1>
8 <div>
9 <Profile id="u1" />
10 </div>
Chapter 4: Optimizing Friend List Interactions 33

11 </div>
12 </NextUIProvider>
13 );
14 }

Next let’s implement the popover component with Friend.

Implementing a Popover Component


A popover is a non-modal dialog that appears adjacent to its trigger
element. It’s often used to display additional rich content.
Here’s a basic implementation using @nextui-org/react:

1 import React from "react";


2 import {Popover, PopoverTrigger, PopoverContent, Button} \
3 from "@nextui-org/react";
4
5 export default function App() {
6 return (
7 <Popover placement="right">
8 <PopoverTrigger>
9 <Button>Open Popover</Button>
10 </PopoverTrigger>
11 <PopoverContent>
12 <div>
13 <div>Popover Content</div>
14 <div>This is the popover content</div>
15 </div>
16 </PopoverContent>
17 </Popover>
18 );
19 }
Chapter 4: Optimizing Friend List Interactions 34

Clicking the “Open Popover” button reveals a popover box to the


right. This box contains a header “Popover Content” in bold,
followed by a descriptive text. It’s styled with padding and font
adjustments for better presentation.

Defining a Trigger Component


The Friend component can act as a trigger for the popover. We
wrap it with PopoverTrigger as follows:
Figure 28. src/friend.tsx
1 import { User } from "../types";
2 import { Popover, PopoverContent, PopoverTrigger } from "\
3 @nextui-org/react";
4 import { Brief } from "./brief.tsx";
5
6 import UserDetailCard from "./user-detail-card.tsx";
7
8 export const Friend = ({ user }: { user: User }) => {
9 return (
10 <Popover placement="bottom" showArrow offset={10}>
11 <PopoverTrigger>
12 <button>
13 <Brief user={user} />
14 </button>
15 </PopoverTrigger>
16 <PopoverContent>
17 <UserDetailCard id={user.id} />
18 </PopoverContent>
19 </Popover>
20 );
21 };

The Brief component accepts a User object and renders its details:
Chapter 4: Optimizing Friend List Interactions 35

Figure 29. src/brief.tsx

1 export function Brief({user}: { user: User }) {


2 return (
3 <div>
4 <div>
5 <img
6 src={`https://i.pravatar.cc/150?u=${user.id}`}
7 alt={`User ${user.name} avatar`}
8 width={32}
9 height={32}
10 />
11 </div>
12 <div>
13 <div>{user.name}</div>
14 <p>{user.bio}</p>
15 </div>
16 </div>
17 );
18 }

A click on the Brief component activates the popover.

Implementing UserDetailCard
Component (Fetching Data)
UserDetailCard is designed to fetch and display user details. The
user detail includes:
Chapter 4: Optimizing Friend List Interactions 36

Figure 30. types.ts

1 export type UserDetail = {


2 id: string;
3 name: string;
4 bio: string;
5 twitter: string;
6 homepage: string;
7 };

We use our reusable get function to fetch these details from the
/users/<id>/details endpoint:

Figure 31. src/user-detail-card.tsx

1 export function UserDetailCard({ id }: { id: string }) {


2 const [loading, setLoading] = useState<boolean>(false);
3 const [detail, setDetail] = useState<UserDetail | undef\
4 ined>();
5
6 useEffect(() => {
7 const fetchFeeds = async () => {
8 setLoading(true);
9 const data = await get<UserDetail>(`/users/${id}/de\
10 tails`);
11
12 setLoading(false);
13 setDetail(data);
14 };
15
16 fetchFeeds();
17 }, [id]);
18
19 if (loading || !detail) {
20 return <div>Loading...</div>;
21 }
22
Chapter 4: Optimizing Friend List Interactions 37

23 return (
24 <Card shadow="none">
25 <CardHeader>
26 <div>
27 <Avatar
28 isBordered
29 radius="full"
30 size="md"
31 src={`https://i.pravatar.cc/150?u=${detail.id\
32 }`}
33 />
34 <div>
35 <h4>{detail.name}</h4>
36 <p>{detail.twitter}</p>
37 </div>
38 </div>
39 </CardHeader>
40 <CardBody>
41 <p>{detail.bio}</p>
42 </CardBody>
43 <CardFooter>
44 <div>
45 <p>
46 <a href={detail.homepage}>{detail.homepage}</\
47 a>
48 </p>
49 </div>
50 </CardFooter>
51 </Card>
52 );
53 }
54
55 export default UserDetailCard;

If we could visualise the current component tree


Chapter 4: Optimizing Friend List Interactions 38

Figure 32. Component Tree with User Detail Card

This implementation appears efficient. However, a network inspec-


tion reveals increasing data transfer to the client as more third-
party libraries are integrated. The additional JavaScript and CSS
for the popover and UserDetailCard could be unnecessary for users
who don’t interact with these features.

Figure 33. More data transferred through network

Is it possible to delay loading these resources until needed?


For instance, only loading the UserDetailCard JS bundle when
a user clicks on a Friend avatar, followed by a request to
/users/1/details for detailed information. Let’s find out in the
Chapter 4: Optimizing Friend List Interactions 39

next chapter.
With the introduction of advanced UI elements and thoughtful
data fetching strategies, this chapter elevates the user experience in
React applications, paving the way for more engaging and efficient
frontend designs. In the next chapter, we’ll dive into code splitting
and lazy load to reduce the initial load, that also the foundation of
React concurrent we’ll learn later.
Chapter 5: Leveraging
Lazy Load and Suspense
in React
Chapter 5 of the ‘Advanced Network Patterns in React’ tutorial
explores the concepts of Lazy Loading and React Suspense for
optimizing performance. It demonstrates how to dynamically
load components only when they are required, reducing initial
load times and improving user experience.

In this chapter, you will learn the following content:

• Introduction to Lazy Loading in React


• Utilizing React Suspense for better loading handling
• Practical implementation in a user profile application

At the end of the previous chapter, we noticed that the page now
has more bytes to load initially, which might not fair for user who
don’t hover on a Friend component - they still need to pay for the
extra network requests and JavaScript bundles.
We could delay such extra (not immediate useful) content into
another request as late as possible (maybe never if the users
don’t ask). For example, we could split UserDetailCard (and its
dependency) into a separate JavaScript bundle and load it whenever
the user hover on it.
Chapter 5: Leveraging Lazy Load and Suspense in React 41

Figure 34. Separate UserDetailCard as another bundle

Let’s see how to implement it in React with lazy load and suspense.

Introducing Lazy Loading


Lazy loading in React is a strategy to dynamically load compo-
nents on-demand, improving performance for larger applications
by minimizing the initial code load. React’s lazy function, used
alongside Suspense, renders dynamic imports as regular compo-
nents, enhancing user experience and resource efficiency.
Consider this implementation:
Chapter 5: Leveraging Lazy Load and Suspense in React 42

1 import React, { Suspense, lazy } from 'react';


2
3 // Lazy load the component
4 const LazyComponent = lazy(() => import('./LazyComponent'\
5 ));
6
7 function App() {
8 return (
9 <div>
10 <Suspense fallback={<div>Loading...</div>}>
11 <LazyComponent />
12 </Suspense>
13 </div>
14 );
15 }
16
17 export default App;

Here, LazyComponent is dynamically imported using lazy(). The


Suspense component wraps around LazyComponent, providing a
fallback UI during its loading phase. When LazyComponent is
needed, it loads on demand, improving performance by splitting
the code into smaller chunks.

Code splitting is a technique in React that enables splitting


your code into various bundles, which can then be loaded
on demand or in parallel. This is particularly beneficial for
improving the performance of large applications. When a
user navigates to a part of your application that requires a
component or library, only then does the necessary bundle
get loaded, significantly reducing the initial load time of the
application. This is especially useful for users with slower
internet connections or on mobile devices. React’s lazy func-
Chapter 5: Leveraging Lazy Load and Suspense in React 43

tion, coupled with Suspense, provides a straightforward way


to implement code splitting, leading to more efficient resource
usage and enhanced user experience.

Implementing UserDetailCard with


lazy loading
Applying this concept, we’ve updated the Friend component in
src/friend.tsx to use React.lazy for importing UserDetailCard.
This change ensures that UserDetailCard loads only when neces-
sary:
Figure 35. src/friend.tsx

1 import { User } from "../types";


2 import { Popover, PopoverContent, PopoverTrigger } from "\
3 @nextui-org/react";
4 import React, { Suspense } from "react";
5 import { Brief } from "./brief.tsx";
6
7 const UserDetailCard = React.lazy(() => import("./user-de\
8 tail-card.tsx"));
9
10 export const Friend = ({ user }: { user: User }) => {
11 return (
12 <Popover placement="bottom" showArrow offset={10}>
13 <PopoverTrigger>
14 <button>
15 <Brief user={user} />
16 </button>
17 </PopoverTrigger>
18 <PopoverContent>
Chapter 5: Leveraging Lazy Load and Suspense in React 44

19 <Suspense fallback={<div>Loading...</div>}>
20 <UserDetailCard id={user.id} />
21 </Suspense>
22 </PopoverContent>
23 </Popover>
24 );
25 };

The code defines a Friend component in React that displays user


information using a popover. It imports user-related types and
components from @nextui-org/react and a local Brief component
for displaying a summary of the user.
The UserDetailCard component is dynamically imported using
React.lazy for performance efficiency, loading only when needed.

1 const UserDetailCard = React.lazy(() => import("./user-de\


2 tail-card.tsx"));

Within the Friend component, a Popover is set up with its trigger


wrapping a button that shows the Brief user summary. When
clicked, the popover displays, containing the UserDetailCard
within a Suspense component. This setup ensures a loading
fallback is shown while the UserDetailCard loads, providing
detailed user information based on the user’s ID. This approach
optimizes loading performance by fetching detailed user data only
when the popover is activated.

1 <Suspense fallback={<div>Loading...</div>}>
2 <UserDetailCard id={user.id} />
3 </Suspense>
Chapter 5: Leveraging Lazy Load and Suspense in React 45

Suspense is a React component that lets your components


“wait” for something before rendering. Initially introduced for
React.lazy (lazy loading of components), its use has expanded
to include data fetching and other asynchronous operations
(we’ll see how to do that in later Chapter with Next.js).
Suspense provides a way to specify a fallback UI – for example,
a loading indicator – that shows up while waiting for the
component to load or data to be fetched. This helps in creating
smoother user experiences in React applications, as it allows
for more control over what gets displayed during the waiting
period of an asynchronous operation. The integration of
Suspense with lazy loading and other React features reflects
the framework’s ongoing evolution to meet modern web de-
velopment needs.

This lazy loading approach is evident in the network panel of


devtools, where two requests are made upon clicking Friend: one
for the JavaScript bundle of UserDetailCard, and another for the
user’s details from the API.

Figure 36. Waterfall with lazy load

Analysis the current bundles, it doesn’t seem help a lot:


Chapter 5: Leveraging Lazy Load and Suspense in React 46

Figure 37. Bundle size analysis

Note in the chart above, the thin slice on the left hand side it the
UserDetailCard, while the big one on the right is everything else.
And if we look closely we’ll find the biggest one is framer-motion
- a package that adding the animation in React - shipped within
NextUI. Obviously we don’t really need animation for everything,
it only used when the popover shows up.
We could further split the Friend into a separate bundle with
NextUI components, and leave the index lightweight.
So firstly we don’t import Friend in Friends, instead we lazy load
it with suspense:
Chapter 5: Leveraging Lazy Load and Suspense in React 47

1 import { User } from "../types.ts";


2 import React, { Suspense } from "react";
3 import { FriendSkeleton } from "../misc/friend-skeleton.t\
4 sx";
5
6 const Friend = React.lazy(() => import("./friend.tsx"));
7
8 const Friends = ({ users }: { users: User[] }) => {
9 return (
10 <div>
11 <h2>Friends</h2>
12 <div>
13 {users.map((user) => (
14 <Suspense fallback={<FriendSkeleton />}>
15 <Friend user={user} key={user.id} />
16 </Suspense>
17 ))}
18 </div>
19 </div>
20 );
21 };
22
23 export { Friends };

And in the Profile.tsx, we will remove NextUIProvider and add


it as a wrapper to Friend, because we don’t need NextUI for the
whole application but the popover in Friend.
Chapter 5: Leveraging Lazy Load and Suspense in React 48

Figure 38. src/friend.tsx

1 //...
2 const UserDetailCard = React.lazy(() => import("./user-de\
3 tail-card.tsx"));
4
5 const Friend = ({ user }: { user: User }) => {
6 return (
7 <NextUIProvider>
8 <Popover placement="bottom" showArrow offset={10}>
9 <PopoverTrigger>
10 <button>
11 <Brief user={user} />
12 </button>
13 </PopoverTrigger>
14 <PopoverContent>
15 <Suspense fallback={<div>Loading...</div>}>
16 <UserDetailCard id={user.id} />
17 </Suspense>
18 </PopoverContent>
19 </Popover>
20 </NextUIProvider>
21 );
22 };
23
24 export default Friend;

With these updates, our new analysis reveals that we have three
distinct bundles: UserDetail, Friend, and Profile.
Chapter 5: Leveraging Lazy Load and Suspense in React 49

Figure 39. Bundle size analysis after splitting Friend

The largest bundle, shown in blue, corresponds to the Friend


component. The smaller, red-tinted one at the top-left is the
UserDetailCard, and the green-tinted one represents the Profile
component.
This is a significant improvement. Now, the loading process works
as follows: When the Profile component initiates parallel requests
for /users/u1 and /users/u1/friends, we display skeleton screens
for the About section and the Friends list. As the friends data
arrives and the Friends component starts rendering, the browser
concurrently downloads the related bundle. During this time, a
FriendSkeleton is displayed, transitioning to the Friend compo-
nent once the bundle is fully downloaded.
Moreover, if the user doesn’t hover over a Friend, there’s no
additional action. The UserDetail data is fetched only when
the user hovers over a Friend, optimizing resource loading and
enhancing performance.
Chapter 5: Leveraging Lazy Load and Suspense in React 50

The potential issue


Visualizing the request process, we see a sequence similar to the
network waterfall discussed in Chapter 2:

Figure 40. Waterfall with lazy load visualized

This observation leads to a question: is it possible to parallelize


these requests to further reduce delays? Exploring this possibility
could unlock more performance enhancements, especially in com-
plex application structures.
This chapter is a deep dive into optimizing React applications
using Lazy Load and Suspense. It provides practical examples
and insights into improving load times and overall application
efficiency. In the next chapter, we’ll explore more advanced
networking patterns and continue enhancing the performance and
user experience of React applications.
Chapter 6: Prefetching
Techniques in React
Applications
Chapter 6 delves into advanced prefetching techniques in
React applications, using SWR for efficient data fetching. It
highlights how to enhance application responsiveness and user
experience through strategic data loading and parallel network
requests.

• Implementing SWR for optimized data fetching


• Strategic use of preload for earlier data loading
• Balancing JavaScript bundle loading with data fetching for
enhanced interactivity

In the last chapter, we explored how lazy loading and the Suspense
API can defer the loading of larger, performance-impacting chunks,
thereby enhancing the initial load speed and user experience.
This approach, leveraging lazy loading, successfully reduced un-
necessary requests and improved overall performance. Yet, as
we wrapped up the chapter, a question arose: could we further
optimize this?
Now, in this chapter, our focus shifts to employing the preload
technique to accelerate user interactions even more. Our specific
goal is to boost performance when users access the UserDetail.
Chapter 6: Prefetching Techniques in React Applications 52

Preloading in JavaScript is a technique used to instruct the


browser to load certain resources early in the page lifecycle.
This is particularly useful for resources that are needed soon
after the initial page load, but not immediately. By preloading
these resources, you can ensure they are available right when
needed, thus reducing load times and improving user interac-
tivity. Preloading is often used for images, scripts, stylesheets,
and other critical assets that contribute to smoother user expe-
riences. Implementing preload can be a strategic choice in web
development to enhance the performance and responsiveness
of web applications.

Introducing SWR
We’re introducing a package named SWR in this chapter, which
will streamline our data fetching process and implement a preload
feature.

SWR, standing for Stale-While-Revalidate, is a caching strat-


egy from Vercel, designed for efficient data fetching in React
applications. It first delivers stale data from the cache (if
available), then revalidates by fetching fresh data in the back-
ground. This approach enhances speed and user experience, as
users see immediate data, albeit potentially stale. SWR auto-
matically updates the data once fresh content is available and
intelligently handles background updating, automatic revali-
dation, and error retries. It’s ideal for data that frequently
changes, where brief displays of slightly outdated information
are acceptable.
Chapter 6: Prefetching Techniques in React Applications 53

Implementing SWR for Preloading


First, let’s add swr to our project:

1 yarn add swr

To use swr, we define a fetcher function, which is essentially a


wrapper around the native fetch method:

1 const fetcher = (...args) => fetch(...args).then(res => r\


2 es.json())

Let’s see swr in action within the Profile component:


Figure 41. src/profile.tsx

1 import useSWR from 'swr'


2
3 function Profile () {
4 const { data, error, isLoading } = useSWR('/api/user/12\
5 3', fetcher)
6
7 if (error) return <div>failed to load</div>
8 if (isLoading) return <div>loading...</div>
9
10 // render data
11 return <div>hello {data.name}!</div>
12 }

Here, useSWR fetches and returns user data, handling loading states
and errors seamlessly.
Chapter 6: Prefetching Techniques in React Applications 54

Integrating preload with SWR


Preloading with SWR is a technique to fetch and cache data before
it’s actually needed in the UI. This is done to improve the user
experience by reducing the loading time when the user eventually
requests that data. The idea is to proactively fetch data in the
background, leveraging SWR’s efficient caching and revalidation
strategy.
Here’s a revised approach to implement preloading in a React
component with SWR:

1 import { preload } from "swr";


2
3 function Component({ user }) {
4 const handleMouseEnter = () => {
5 preload(`/api/user/${user.id}/details`, fetcher);
6 };
7
8 return (
9 <div onMouseEnter={handleMouseEnter}>
10 {/* Render the rest of the component */}
11 </div>
12 );
13 }

In this implementation, the preload function is used to initiate a


data fetch using SWR. It doesn’t directly return or use the fetched
data but relies on SWR’s caching mechanism.

SWR optimizes data fetching in React with an efficient caching


strategy. On the initial data fetch, SWR stores the response
Chapter 6: Prefetching Techniques in React Applications 55

in a cache using the fetch URL as the key. For subsequent


requests with the same key, SWR first checks this cache. If the
cached data is recent (within the deduping interval), it’s used
immediately, reducing network requests. Meanwhile, SWR
revalidates the data in the background to ensure freshness, up-
dating the cache and UI if new data is fetched. This approach
leads to faster load times and a seamless user experience,
especially for frequently updated data.

When the Component component detects a mouse entering its area


(via onMouseEnter), it calls preload. This proactively fetches and
caches the detailed user data for that friend. Later, when the
component that actually needs to display this data is rendered, the
data will be retrieved from the SWR cache, making it available
immediately if it’s within the deduping interval.
This method of preloading with SWR is particularly useful for
scenarios where you can predict which data the user will need
next, thereby subtly improving the responsiveness and fluidity of
the user experience.
Let’s now modify the Friend component:
Figure 42. src/friend.tsx
1 export const Friend = ({ user }: { user: User }) => {
2 const handleMouseEnter = () => {
3 preload(`/user/${user.id}/details`, () => getUserDeta\
4 il(user.id));
5 };
6
7 return (
8 <Popover placement="bottom" showArrow offset={10}>
9 <PopoverTrigger>
10 <button onMouseEnter={handleMouseEnter}>
11 <Brief user={user} />
Chapter 6: Prefetching Techniques in React Applications 56

12 </button>
13 </PopoverTrigger>
14 {/* existing logic */}
15 </Popover>
16 );
17 };

In UserDetailCard, we utilize useSWR for fetching user details:


Figure 43. src/user-detail-card.tsx

1 import useSWR from "swr";


2 import { getUserDetail } from "../api.ts";
3
4 export function UserDetailCard({ id }: { id: string }) {
5 const { data: detail, isLoading: loading } = useSWR(
6 `/user/${id}/details`,
7 () => getUserDetail(id)
8 );
9
10 //...
11 }

With this setup, hovering over a Friend triggers a preloading of


the network call to /user/<id>/details. Clicking on it renders
the detailed information. SWR handles caching and revalidation,
enhancing the user experience.
Chapter 6: Prefetching Techniques in React Applications 57

Figure 44. Request JS bundle and data in parallel

Visualizing this, we see separate and parallel loading of data and


the JavaScript bundle, significantly improving interactivity.

Figure 45. Request JS bundle and data in parallel

To recap our progress, let’s review key strategies we’ve employed


to enhance performance:

• Initially load only essential content by splitting and employ-


ing lazy loading.
• Whenever feasible, execute network requests in parallel.
• Implement preloading to retrieve data ahead of user interac-
tion with components.
Chapter 6: Prefetching Techniques in React Applications 58

• For any third-party libraries, consider code splitting into


separate bundles and apply lazy loading as needed.

By applying these methods, we’ve significantly boosted our appli-


cation’s performance. But so far, our focus has been predominantly
on frontend optimizations. What if we broaden our perspective?
Can we streamline our application further by shifting more data
fetching to the backend? Or is it possible to pre-render content,
updating it only as necessary on the frontend?
These considerations lead us into backend techniques, which will
be our focus in upcoming chapters. We’ll explore and discuss Server
Side Rendering, Static Site Generation, and React Server Compo-
nents. We’ll delve into how and when to use these approaches
to ensure our application is performance-optimized before it even
reaches the client side. Stay tuned for more insights.
This chapter wraps up our exploration of network patterns in
React, on the client side. We’ve covered parallel requests, code
splitting, and prefetching to improve performance. Up next, we’ll
explore server-side technologies to further enhance application
performance.
Chapter 7: Introducing
Server Side Rendering
This chapter dives into leveraging Server-Side Rendering (SSR)
in React applications. It discusses how SSR, combined with
React Server Components, can optimize initial page load times
and enhance overall application performance.

• Understanding and implementing SSR in React


• Integrating React Server Components for efficient rendering
• Enhancing user experience with faster initial page loads

In the previous chapters, we’ve explored various client-side tech-


niques to enhance initial page rendering speed. However, signif-
icant optimizations can also be achieved server-side, sometimes
even before a user sends a request. What if we could complete
some tasks during the request, leveraging backend efficiencies?
Let’s delve into Server-Side Rendering (SSR) in this chapter, partic-
ularly focusing on React Server Components within Next.js.
Server-Side Rendering is a web development technique where the
server generates the complete HTML of a webpage in response
to a user request. Each time a user accesses a URL or refreshes
their browser, the server executes the necessary JavaScript code,
renders all dynamic content, and sends this fully prepared HTML
to the browser. While this involves some processing time on the
server, it allows for a quicker display of a fully rendered page
compared to waiting for client-side JavaScript to render content.
Chapter 7: Introducing Server Side Rendering 60

SSR significantly enhances the user experience, especially for initial


page loads, and improves a website’s search engine optimization by
ensuring all content is immediately accessible.
Implementing SSR requires backend techniques. There are many
alternatives: building a full-featured backend service is time-
consuming and effort-intensive, but we don’t need to start from
scratch. In this chapter, we will use Next.js for SSR and React Server
Components.

Introducing Next.js
If you’re not familiar with Next.js, I recommend going through its
dashboard tutorial1 . It’s beneficial to follow along with the tutorial,
experiment with it, and then return here.
Next.js is an open-source web development framework built on
Node.js, designed to simplify the building of server-rendered Re-
act applications. Created by Vercel (formerly Zeit), it aims to
streamline the process of building performant, scalable, and user-
friendly web applications. It enhances the developer experience
with features like automatic code splitting, server-side rendering,
static site generation, and built-in CSS support. In this chapter,
we’ll focus on its app router feature for SSR, which functions
similarly to client-side React components.
To start, we’ll create a dynamic route – app/user/[id]/page.tsx.
This path signifies a dynamic route within the app directory in
Next.js. For instance, accessing /user/123 renders the page.tsx
component for the user with ID 123, changing content based on
the URL parameter.

1 https://nextjs.org/learn/dashboard-app
Chapter 7: Introducing Server Side Rendering 61

Next.js version 13 introduced an App Router utilizing React


Server Components, supporting shared layouts, nested rout-
ing, loading states, error handling, and more. This system
bases its routes on the file structure in the “app” directory.
Learn more about Routing Fundamentals2 and Pages and Lay-
outs3 .

In contrast, static rendering generates HTML at build time with pre-


fetched data, resulting in super-fast page loads due to static HTML.
We’ll explore this further in the next chapter.
In page.tsx, we define a Page component to fetch data and pass it
to the frontend:
Figure 46. app/user/[id]/page.tsx

1 export default async function Page({ params }: PageProps)\


2 {
3 const { user, friends } = await getUserBasicInfo(params\
4 .id);
5
6 return (<Profile user={user} friends={friends} />);
7 }

The PageProps interface is:

2 https://nextjs.org/docs/app/building-your-application/routing
3 https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts
Chapter 7: Introducing Server Side Rendering 62

1 interface PageProps {
2 params: {
3 id: string;
4 };
5 }

The params object is part of Next.js’s routing feature, which al-


lows you to access dynamic parts of the URL. In the case of
app/user/[id]/page.tsx, params would contain an id property.
This id corresponds to the dynamic segment in the URL. For
example, if the URL is /user/123, params.id will be 123. This
allows the Page component to fetch or compute data specific to that
user ID.
The function getUserBasicInfo is defined as follows for actual data
fetching at request time:
Figure 47. app/user/[id]/page.tsx

1 async function getUserBasicInfo(id: string) {


2 const [user, friends] = await Promise.all([
3 get<User>(`/users/${id}`),
4 get<User[]>(`/users/${id}/friends`),
5 ]);
6
7 return { user, friends };
8 }

The Profile component, defined as a simple presentational com-


ponent, receives data directly from the Page component. We are
using About and Friends components from Chapter 3, they are all
presentational component and only render whatever passed in:
Chapter 7: Introducing Server Side Rendering 63

Figure 48. components/profile.tsx

1 const Profile = ({ user, friends }: { user: User; friends\


2 : User[] }) => {
3 return (
4 <>
5 <About user={user} />
6 <Friends users={friends} />
7 <Feeds category={user.interests[0]} />
8 </>
9 );
10 };

Next.js by default uses Server Components, meaning they are


rendered server-side without additional setup. We can define
traditional, or client components, for contrast. Let’s make the Feeds
component a client component to demonstrate its use alongside
Server Components.
Figure 49. components/feeds.tsx
1 'use client';
2
3 import { useEffect, useState } from "react";
4
5 const Feeds = ({ category }: { category: string }) => {
6 const [loading, setLoading] = useState<boolean>(false);
7 const [feeds, setFeeds] = useState<Feed[]>([]);
8
9 useEffect(() => {
10 const fetchFeeds = async () => {
11 setLoading(true);
12 const response = await fetch(
13 `http://localhost:1573/articles/${category}`
14 );
15 const data = await response.json();
16
Chapter 7: Introducing Server Side Rendering 64

17 setLoading(false);
18 setFeeds(data);
19 };
20
21 fetchFeeds();
22 }, [category]);
23
24 if (loading) {
25 return <div>Loading...</div>;
26 }
27
28 return (
29 <div>
30 <h2>Your Feeds</h2>
31 <div>
32 {feeds.map((feed) => (
33 <>
34 <h3>{feed.title}</h3>
35 <p>{feed.description}</p>
36 </>
37 ))}
38 </div>
39 </div>
40 );
41 };
42
43 export { Feeds };

Pay special attention to the use client directive employed in our


example. This directive explicitly marks a component as a client-
side component in Next.js.
This distinction is crucial because it dictates the use of certain React
hooks. In client components, you have the freedom to use hooks
like useState and useEffect, just as you would in a standard React
application. These hooks are essential for managing state and side
Chapter 7: Introducing Server Side Rendering 65

effects on the client side.


However, it’s important to note that these APIs are not available for
Server Components. Server Components are designed for different
purposes, mainly focused on preparing and delivering content from
the server without interactive or stateful behavior.

React Server Components


React Server Components represent a paradigm shift in application
architecture, as defined on the React homepage. They run ahead of
time, separate from your JavaScript bundle, and are instrumental
in optimizing your app’s performance.

React’s research introduced Server Components as a


novel component type that operates before the browser
load and is not included in the JavaScript bundle. These
components can execute during the build process, al-
lowing access to the filesystem or static content without
the need for an API. They facilitate data transfer to
interactive Client Components in the browser through
props.

Server Components are best suited for non-interactive elements


that form the content’s framework, while Client Components
handle interactive aspects like mouse and keyboard events.
When a page loads, you might notice a slight delay initially. Soon,
components like About and Friends appear, followed by a loading
phase for Feeds. Once the API call for feeds completes, the entire
content is displayed.
Chapter 7: Introducing Server Side Rendering 66

Figure 50. Server Side Rendering

Why shift requests from the frontend to the backend? Several


benefits include:

• Faster data retrieval, as servers are usually closer to data


storage.
• Simplified data caching and control on the server side.
• Enhanced security and power in backend processing.
• Frontend transparency in security setups like API keys or
tokens.

Though the initial load may take slightly longer, subsequent op-
erations become more responsive, as no additional data loading is
required. This approach enhances the user experience and allows
for various caching levels, speeding up future data requests.
In this chapter, we explore Server-Side Rendering (SSR) and its
integration with React Server Components. We delve into how SSR
improves initial page load times, making React applications more
performant and user-friendly. Up next: Static Site Generation.
With key insights from this chapter, we pave the way to explore
the synergy between SSR and React Server Components, setting
the stage for the next topic: Static Site Generation.
Chapter 8: Introducing
Static Site Generation
Chapter 8 explores Static Site Generation (SSG) in React,
emphasizing how to optimize webpages at build time for
enhanced performance and user experience. It differentiates
between runtime and build-time data fetching, providing prac-
tical examples using Next.js.

Chapter highlights:

• Understanding and implementing SSG in React applications


• Differentiating between runtime and build-time data fetching
• Techniques for mixing Server-Side Rendering and SSG for
optimized performance

Static Site Generation (SSG) is a vital technique in modern web


development for pre-rendering pages at build time, contrasting
with on-demand rendering in traditional server-side rendering.
This approach, especially within frameworks like Next.js, involves
creating static HTML files for each page during the build process.
SSG does not restrict your site from being dynamic. Post the
initial rendering, client-side JavaScript can still enable dynamic
behaviors. The ‘static’ aspect pertains to the first render of the page,
after which interactivity is managed by client-side JavaScript.
So far, we’ve seen runtime requests where the data required, like
a user’s profile, is unknown until the client makes a request. In
Chapter 8: Introducing Static Site Generation 68

contrast, certain data, such as universal advertisements or an-


nouncements, can be known and generated at build time. These
types of data, which don’t change per request, are ideal candidates
for SSG.
For instance, in a Home page displaying advertisements (which are
static for all visitors), Next.js can pre-render this content at build
time using static routing. A sample implementation might look like
this:
Figure 51. app/page.tsx

1 export default async function Home() {


2 const ads = await getAds();
3 return (<Ads ads={ads} />);
4 }

Accessing the home page (/) results in almost no latency, as the


HTML content is already rendered, akin to a traditional static
website.

Figure 52. Advertisement Screen


Chapter 8: Introducing Static Site Generation 69

However, routes like /user/<id> are different, as they require


information at request time, like /users/u1/friends.

Mixing Server-Side Rendering and


Static Site Generation
In Next.js, you can blend SSG and SSR in dynamic routing. For
instance, pre-generating certain user profiles can be achieved with
generateStaticParams.

Figure 53. app/user/[id]/page.tsx

1 export function generateStaticParams() {


2 return [{ id: "u1" }, { id: "u2" }, { id: "u3" }];
3 }
4
5 export default async function Page({ params }: PageProps)\
6 {
7 return <Profile id={params.id} />;
8 }

At build time, Next.js calls generateStaticParams, using the results


to pre-render pages:

1 generateStaticParams().forEach(param => <Page params={par\


2 am} />)

The result is pre-rendered HTML pages, so when users in


generateStaticParams visit their profiles, the content is already
there and loads blazingly fast.
Chapter 8: Introducing Static Site Generation 70

Figure 54. Profile Pre-Rendered

This approach eliminates frontend data requests for pre-rendered


users:

Figure 55. Static Generation Diagram

However, pre-rendering all data at build time can be impractical


due to the volume of data and its dynamic nature. That’s where a
mix of SSG, SSR, and client-side fetching becomes crucial:
Chapter 8: Introducing Static Site Generation 71

Figure 56. SSR and SSG Mix Diagram

Initially, non-real-time data (like user bio, friends list) is pre-


rendered into static HTML. For each request, SSR can be used for
up-to-date information, and user interactions trigger further client-
side data fetching.
This chapter delves into the intricacies of Static Site Generation
in React. We discuss how SSG optimizes performance by pre-
rendering pages and explore the balance between SSG, Server-Side
Rendering, and client-side data fetching. Next, we’ll look at user
skeleton components to further enhance user experience. Moving
forward from server-side rendering, this chapter introduces the
concepts and practical implementation of Static Site Generation,
setting the stage for enhancing user experience with skeleton
components in the next chapter.
Chapter 9: Optimizing
User Experience with
Skeleton Loading in
React
This chapter offers a detailed walkthrough on enhancing user
experience in React applications by implementing skeleton
components. Learn how to create and use these components to
provide users with a visual cue during data loading, reducing
perceived wait times and improving interaction.

Chapter highlights:

• Building skeleton components in React for effective loading


indicators
• Demonstrating the use of subtle animations and colors in
skeleton design
• Techniques to reduce Cumulative Layout Shift (CLS) for a
smoother user experience

In this chapter, we will look into the loading indicator, there are two
common ways of showing the users that something is happening
and we can not show the data right away: skeleton and spinner.
A skeleton component is a user interface design used to indicate
data loading, where a placeholder mimicking the actual content
layout is displayed. These are usually grey blocks or lines that show
Chapter 9: Optimizing User Experience with Skeleton Loading in 73
React

where and how the final content, like text or images, will appear.
The skeleton component enhances the user experience by reducing
the element of surprise during loading, providing a preview of the
content’s structure.

Figure 57. Skeleton Components

On the other hand, a spinner is a more generic loading indicator,


often a circular animation, signifying that an operation such as data
fetching or processing is in progress. Unlike skeleton components,
spinners do not give any hint about the content’s layout or the
loading duration.
The main difference between the two lies in the level of context they
offer. Skeleton components provide a glimpse of the content’s lay-
out, making the loading process feel more integrated and smoother.
In contrast, spinners are used when the exact content layout is
unknown or the loading time is brief, serving as a universal sign of
an ongoing process without specific layout information. Skeleton
components are about enhancing the user experience with specific
layout anticipation, while spinners are more about universally
indicating activity.
Skeleton is surprisingly easy to implement, let’s see how we can
make one for the About component when the content is loading:
Chapter 9: Optimizing User Experience with Skeleton Loading in 74
React

1 function AboutSkeleton() {
2 return (
3 <div className="flex flex-row gap-2 pb-4 items-center\
4 animate-pulse">
5 <div>
6 <div className="w-12 h-12 rounded-full animate-pu\
7 lse bg-slate-300" />
8 </div>
9 <div className="flex flex-col gap-2">
10 <div className="text-2xl font-bold w-20 h-6 bg-sl\
11 ate-200" />
12 <p className="text-xs w-24 h-2 bg-slate-200" />
13 </div>
14 </div>
15 );
16 }

The AboutSkeleton component creates a row layout with centered


items and a gap between them. The animate-pulse class applies a
shimmering animation effect. The component includes a circular
shape to mimic an avatar and two rectangular blocks to represent
text lines, all with different shades of gray for a placeholder effect.
The use of Tailwind CSS classes like flex, gap-2, rounded-full,
and bg-slate-300 controls the layout and appearance, ensuring the
skeleton mimics the structure of the content it represents.
Chapter 9: Optimizing User Experience with Skeleton Loading in 75
React

Figure 58. Skeleton for About component

This placeholder doesn’t need to be perfect but should give an idea


of the actual component’s layout. Aiming for a close height match
with the actual component can help in reducing CLS (Cumulative
Layout Shift).

Cumulative Layout Shift (CLS) is a metric used to quantify


how much elements on a webpage move around during load-
ing. It’s part of the Core Web Vitals, a set of metrics from
Google designed to measure user experience on web pages.
CLS specifically measures the stability of a page as it loads,
with a focus on how unexpected layout shifts can affect a user’s
experience.
Cumulative Layout Shift (CLS)1

For those interested in a hands-on guide, my video2 demonstrates


creating a skeleton component from the ground up.
Key takeaways for creating custom skeleton components are:

• Choosing subdued colors for the background shapes


1 https://web.dev/articles/cls
2 https://youtu.be/g5u9LzKydAA
Chapter 9: Optimizing User Experience with Skeleton Loading in 76
React

• Aligning the size and position of placeholders as close to the


actual UI as possible
• Employing animation to convey a dynamic loading state

This chapter closes our exploration of enhancing user interfaces


in React with skeleton loading. We’ve covered design principles,
implementation techniques, and best practices. Next, we’ll shift
our focus to server-side technologies for further performance im-
provements. As this chapter concludes, we encapsulate the essence
of effective loading indicators and set the stage for the next chapter
on server-side technologies.
Chapter 10: The New
Suspense API in React
Chapter 10 explores the innovative Suspense API in React
18, focusing on its application in asynchronous data fetching
and UI streaming. The chapter demonstrates how Suspense,
combined with streaming in Next.js, enhances UI interactivity
and responsiveness.

Chapter highlights

• Exploring the new Suspense API for data fetching in React 18


• Implementing UI streaming with Next.js for dynamic content
rendering
• Enhancing user experience with progressive loading and
structured data fetching

We’ve previously explored code splitting and lazy loading using


Suspense in Chapter 5. The Suspense API’s ability to wait for
components to be ready for rendering is impressive, but what if
we could apply this pattern to all asynchronous data fetching?
Consider the following scenario: In About, we fetch data and wrap
it in Suspense. While fetching user details, a skeleton component is
displayed, and upon data retrieval, the actual content is rendered.
Chapter 10: The New Suspense API in React 78

1 const About = async ({ id }) => {


2 const user = await get(`/users/{id}`);
3
4 return <div>{user.name}</div>;
5 };
6
7 const App = () => {
8 return (
9 <Suspense fallback={<AboutSkeleton />}>
10 <About id={id} />
11 </Suspense>
12 );
13 };

This approach wasn’t feasible until the release of React 18, which
expanded the use of Suspense beyond just code splitting.
It’s important to note that this new application of Suspense is
still experimental and not yet widely considered production-ready.
However, libraries like SWR and React Query, and frameworks
such as Next.js, are already experimenting with it.
Let’s examine how to implement data-fetching with Suspense.

Re-Introducing Suspense & Fallback


First, create a UserInfo component as a wrapper for About and
Friends:
Chapter 10: The New Suspense API in React 79

Figure 59. components/userinfo.tsx

1 export async function UserInfo({ id }: { id: string }) {


2 const user = await getUser(id);
3
4 return (
5 <>
6 <About user={user} />
7 <Suspense fallback={<FeedsSkeleton />}>
8 <Feeds category={user.interests[0]} />
9 </Suspense>
10 </>
11 );
12 }

The UserInfo component, defined as an asynchronous function,


fetches user data based on the given id. The fetched user data is
then passed to the About component. However, since getUser(id)
is an async call, the entire UserInfo component, including the About
component, will only render once the user data is successfully
fetched.
For the Feeds component, it’s wrapped in a Suspense component.
This means if Feeds is waiting for its data (like fetching additional
information based on the user’s interests), the Suspense component
will display the FeedsSkeleton as a fallback. Once the data required
by Feeds is available, the Feeds component will render with the
actual data.
Therefore, both About and Feeds depend on the asynchronous fetch-
ing of user data, but Feeds specifically leverages React’s Suspense
for a smoother loading experience. The use of Suspense for Feeds
allows for a part of the component tree to wait on its own data
fetching independently, providing a fallback during the wait.
Here, we use Suspense for the Feeds component. The Friends
component is defined as follows:
Chapter 10: The New Suspense API in React 80

Figure 60. components/friends.tsx

1 import { Friend } from "@/components/friend";


2
3 async function Friends({ id }: { id: string }) {
4 const friends = await getFriends(id);
5
6 return (
7 <div>
8 <h2>Friends</h2>
9 <div>
10 {friends.map((user) => (
11 <Friend user={user} key={user.id} />
12 ))}
13 </div>
14 </div>
15 );
16 }
17
18 export { Friends };

We define another asynchronous Friends function component in


React, upon receiving a user id as a prop, the component fetches the
user’s friends using the getFriends(id) function. After fetching
the friends’ data, we map the users into Friend components.
These components are then utilized in the Profile container:
Chapter 10: The New Suspense API in React 81

Figure 61. components/profile.tsx

1 import { Suspense } from "react";


2 import { Friends } from "@/components/v4/friends";
3 import { UserInfo } from "@/components/v4/userInfo";
4
5 import { FriendsSkeleton } from "@/components/misc/friend\
6 s-skeleton";
7 import { UserInfoSkeleton } from "@/components/misc/user-\
8 info-skeleton";
9
10 export async function Profile({ id }: { id: string }) {
11 return (
12 <div>
13 <h1>Profile</h1>
14 <div>
15 <Suspense fallback={<UserInfoSkeleton />}>
16 <UserInfo id={id} />
17 </Suspense>
18
19 <Suspense fallback={<FriendsSkeleton />}>
20 <Friends id={id} />
21 </Suspense>
22 </div>
23 </div>
24 );
25 }

Let’s visualise the component tree to have a more direct perspective,


we’ll learn how such boundary can help the performance in the
section Streaming in Next.js later.
Chapter 10: The New Suspense API in React 82

Figure 62. Suspense boundaries

Using skeletons in different layers


Initially, skeleton components UserInfoSkeleton and
FriendsSkeleton are displayed. As data fetched inside each
component correspondingly, we can then render the component
with data.
In this segment of the code, we define two components in React:
UserInfoSkeleton and UserInfo.

Figure 63. components/misc/user-info-skeleton.tsx

1 const UserInfoSkeleton = () => {


2 return (
3 <>
4 <AboutSkeleton />
5 <FeedsSkeleton />
6 </>
7 );
8 };
Chapter 10: The New Suspense API in React 83

UserInfoSkeleton is a simple functional component that serves


as a placeholder while the actual user information is being
fetched. It combines two skeleton components, AboutSkeleton
and FeedsSkeleton, indicating that the UserInfo component will
consist of two main parts: one for ‘About’ information and another
for ‘Feeds’.
Figure 64. components/userinfo.tsx

1 export async function UserInfo({ id }: { id: string }) {


2 const user = await getUser(id);
3
4 return (
5 <>
6 <About user={user} />
7 <Suspense fallback={<FeedsSkeleton />}>
8 <Feeds category={user.interests[0]} />
9 </Suspense>
10 </>
11 );
12 }

The UserInfo component is more complex. It is an asynchronous


function that takes a user id as a prop and fetches the user’s
data with getUser(id). Once the data is fetched, it renders two
components: About and Feeds. The About component is rendered
directly with the fetched user data. For the Feeds component,
React’s Suspense is used. The Feeds component is responsible
for displaying content based on the user’s interests, and while it’s
fetching this data, the FeedsSkeleton is shown as a fallback. Once
the Feeds data is ready, the actual Feeds component replaces the
skeleton.
This setup allows for a smooth and progressive loading experience.
Initially, the UserInfoSkeleton is displayed. As data is fetched,
the actual components (About and Feeds) gradually replace their
Chapter 10: The New Suspense API in React 84

respective skeleton components. This approach enhances the user


experience by providing visual feedback during data loading and
progressively revealing content as it becomes available.

Streaming in Next.js
Next.js documentation explains streaming as a technique to pro-
gressively render UI from the server. It splits work into chunks
streamed to the client as they’re ready. This allows for immediate
rendering of parts of the page.
In Next.js, streaming can be achieved through:

• A special loading.tsx file in your app router.


• Using the Suspense API.

We’ve seen Suspense above. Alternatively, you can define a


loading.tsx in app/user/[id]/loading.tsx:

Figure 65. app/user/[id]/loading.tsx

1 export default function Loading() {


2 return (
3 <div>
4 <h1>Profile</h1>
5 <div>
6 <AboutSkeleton />
7 <FeedsSkeleton />
8 <FriendsSkeleton />
9 </div>
10 </div>
11 );
12 }

In page.tsx, you can await all data before rendering:


Chapter 10: The New Suspense API in React 85

1 export default async function Page({ params }: PageProps)\


2 {
3 const { user, friends } = await getUserBasicInfo(params\
4 .id);
5
6 return (<Profile user={user} friends={friends} />);
7 }

Alternatively, you can push streaming to individual components,


allowing earlier user interaction. This is the approach we’re using
now as it’s more flexible, we can define more granular skeleton and
suspense boundary, that also means potentially more content can
be static thus rendered much faster (think of the headline, section
header, etc.).

Figure 66. Partial Rendering with Skeletons

We aim to shift content generation to the server side as much as


possible, but client components remain essential for high interac-
tivity.
Chapter 10: The New Suspense API in React 86

Optimizing UI by Grouping Related


Data Components
A key strategy to enhance user experience is to group related data
components in the UI. This approach ensures that related infor-
mation is displayed together, aligning with the user’s expectations
and the logical flow of data. For instance, in our application, user
information displayed in the About component is closely related to
the Feeds, as both pertain to the user’s interests. Conversely, the
Friends component, which can function independently, might be
more suitably placed in a separate section of the UI.
To implement this, we consider a left-right layout that visually
and functionally groups related components together. Such an
arrangement not only improves the coherence of the displayed
information but also enhances the overall aesthetic and navigability
of the UI.

Figure 67. UI Rearrangement Based on Data Grouping

In practice, this layout adjustment requires minimal changes to


the code. We introduce a vertical version of the FriendsSkeleton,
Chapter 10: The New Suspense API in React 87

named FriendsSkeletonVertical, to align with the new layout.


The Profile component is then updated to reflect this new struc-
ture:

1 export async function Profile({ id }: { id: string }) {


2 return (
3 <div>
4 <h1>Profile</h1>
5 <div>
6 <div>
7 <Suspense fallback={<UserInfoSkeleton />}>
8 <UserInfo id={id} />
9 </Suspense>
10 </div>
11 <div>
12 <Suspense fallback={<FriendsSkeletonVertical />\
13 }>
14 <Friends id={id} />
15 </Suspense>
16 </div>
17 </div>
18 </div>
19 );
20 }

This updated layout ensures that each component manages its


own data fetching, maintaining a clean separation of concerns.
The use of Suspense boundaries for each component enhances
the experience by providing immediate visual feedback (through
skeletons) and progressively loading the content. This approach
not only streamlines the rendering process but also optimizes
the interactivity and responsiveness of the application, ensuring
users have a smooth and engaging experience navigating through
different sections of the user profile.
In Chapter 10, we investigate the new Suspense API in React
Chapter 10: The New Suspense API in React 88

18, highlighting its role in asynchronous data fetching and UI


streaming. The chapter provides insights into using this API
with Next.js to progressively render content and improve user
interaction. This chapter delves into React 18’s new Suspense API,
showcasing its capabilities in transforming data fetching and user
interface rendering. We explore how to utilize this API alongside
Next.js for a more interactive and responsive user experience.
Chapter 11: Lazy Load,
Dynamic Import, and
Preload in Next.js
Chapter 11 focuses on enhancing Next.js applications by im-
plementing lazy loading, dynamic importing, and preloading
techniques. It discusses how these strategies contribute to
efficient data handling and improve user experience.

Chapter highlights:

• Utilizing next/dynamic for lazy loading and dynamic import


• Techniques for preloading data to enhance responsiveness
• Integrating lazy loading with user interaction for efficient
content delivery

Chapter 5 introduced lazy loading in the client-side context,


demonstrating how separating non-essential content into different
bundles can enhance initial rendering and user experience.
While Next.js accelerates initial rendering with static rendering,
streaming, and server-side rendering, implementing lazy loading
and preloading further optimizes the application. These techniques
are especially beneficial for users who don’t require access to all
content immediately.
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 90

Dynamic Load in Next.js


In Next.js, next/dynamic is a tool for dynamic component import-
ing, it is a composite of React.lazy() and Suspense. It’s particu-
larly useful for optimizing the loading of components that are not
immediately necessary on the initial render, thereby enhancing the
performance and user experience of your application.
In the example below, the Gallery component is imported dynami-
cally using next/dynamic. This means that Gallery will not be part
of the main JavaScript bundle that loads when the page initially
loads. Instead, it will be loaded only when the App component
renders it:

1 'use client'
2
3 const Gallery = dynamic(() => import("./gallery"));
4
5 const App = () => {
6 return <Gallery />
7 }

In this case, the Gallery component is fetched and loaded asyn-


chronously, only when the App component needs it. This ap-
proach reduces the initial load time of your application because the
browser downloads fewer resources upfront.
Furthermore, next/dynamic allows for specifying a fallback com-
ponent that is displayed while the dynamic component is being
loaded:
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 91

1 'use client'
2
3 const Gallery = dynamic(() => import("./gallery"), {
4 loading: () => <GallerySkeleton />
5 });
6
7 const App = () => {
8 return <Gallery />
9 }

This technique is useful for client components, allowing them to be


bundled separately and loaded as needed.

Implementing the UserDetailCard


Consider a UserDetailCard component that might not be immedi-
ately needed by every user. We can apply lazy loading to enhance
performance:
Figure 68. components/friend.tsx

1 const UserDetailCard = dynamic(() => import("./user-detai\


2 l-card"));
3
4 export const Friend = ({ user }: { user: User }) => {
5 return (
6 <NextUIProvider>
7 <Popover placement="bottom" showArrow offset={10}>
8 <PopoverTrigger>
9 <button>
10 <Image src={`https://i.pravatar.cc/150?u=${us\
11 er.id}`} />
12 <span>{user.name}</span>
13 </button>
14 </PopoverTrigger>
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 92

15 <PopoverContent>
16 <UserDetailCard id={user.id} />
17 </PopoverContent>
18 </Popover>
19 </NextUIProvider>
20 );
21 };

Here, the UserDetailCard is dynamically loaded only when re-


quired, reducing the initial load.
And in the UserDetailCard we could do the skeleton just like above:
Figure 69. components/user-details-card.tsx

1 async function UserDetailCard({ id }: { id: string }) {


2 const detail = await getUserDetail(id);
3
4 return (
5 <Card>
6 {/* same to the UserDetailCard defined in Chapter 6\
7 */}
8 </Card>
9 );
10 }

And when we click a Friend you can see there is an additional


request send (for a JavaScript chunk), and then following a network
request.
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 93

Figure 70. Code Split with lazy loading

You might be wondering if we could do the same preload technique


as we have seen in Chapter 6, and the answer is yes. We could use
the same useSWR package in Next.js, but let’s try alternative - define
a cache and see how we can use Server-Side Rendering to hide the
request details.

Preload in Next.js
Preloading can be used to fetch data in advance, such as when a
user hovers over a button:
Figure 71. components/friend.tsx

1 "use client";
2
3 const UserDetailCard = dynamic(() => import("./user-detai\
4 l-card"));
5
6 export const Friend = ({ user }: { user: User }) => {
7 const handleHover = () => {
8 preload(user.id);
9 };
10
11 return (
12 <NextUIProvider>
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 94

13 <Popover placement="bottom" showArrow offset={10}>


14 <PopoverTrigger>
15 <button
16 tabIndex={0}
17 onMouseEnter={handleHover}
18 >
19 <Brief user={user} />
20 </button>
21 </PopoverTrigger>
22 <PopoverContent>
23 <UserDetailCard id={user.id} />
24 </PopoverContent>
25 </Popover>
26 </NextUIProvider>
27 );
28 };

While preload is defined as:

1 import { get } from "@/utils/get";


2
3 export const getUserDetail = async (id: string) => {
4 return await get<UserDetail>(`/users/${id}/details`);
5 };
6
7 export const preload = (id: string) => {
8 void getUserDetail(id);
9 };

The preload function fetches the user’s details when the user
hovers over the Friend component. This is accomplished by
triggering the preload function on the onMouseEnter event of the
button element. When the mouse pointer enters the button’s area,
getUserDetail(id) is called, and it fetches the details of the user
associated with the Friend component.
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 95

Note here in line 8 above, the void operator1 evaluates the ex-
pression (getUserDetail(id)) and then returns undefined, which
is handy if we want to execute a function but don’t care of the
return value.

Figure 72. Preload data when user hover

To optimize the preloading process and prevent unnecessary re-


peated data fetches, we implement an internal caching mechanism
using a Set. Here’s the refined explanation for the provided code
snippet:
Figure 73. apis.ts

1 import { cache } from "react";


2
3 const preloadedUserIds = new Set();
4
5 export const getUserDetail = cache(async (id: string) => {
6 preloadedUserIds.add(id);
7 return await get<UserDetail>(`/users/${id}/details`);
8 });
9
10 export const preload = (id: string) => {
11 if(!preloadedUserIds.has(id)) {
12 void getUserDetail(id);
13 }
1 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/void
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 96

14 };

In the apis.ts file, we introduce a caching strategy using a Set to


manage the preloading of user details efficiently. The Set, named
preloadedUserIds, stores user IDs for which details have been
preloaded. This unique collection ensures that each user’s details
are fetched only once, avoiding redundant network requests.

The cache function in React2 , currently in the Canary (ex-


perimental) version, is designed for use with React Server
Components. It allows you to create a cached version of a
function, which can be particularly useful for data fetching or
expensive computations. When you call the cached function
with specific arguments, it first checks if there’s a cached
result. If so, it returns this result; if not, it executes the function,
stores the result in the cache, and then returns it.
This approach can significantly optimize performance by
avoiding repeated executions of the same function with the
same arguments. However, it’s important to note that each
call to cache creates a new function, so calling it multiple
times with the same function will not share the cache.
Additionally, cache only works in Server Components and is
for experimental use as of now.

The getUserDetail function fetches user details and is enhanced


with React’s cache function. This caching mechanism ensures the
asynchronous operation’s results are stored, reducing unnecessary
data fetching. When the preload function is called with a user ID, it
first checks if the ID is already in preloadedUserIds. If not present,
it fetches and caches the user’s details. This approach optimizes
2 https://react.dev/reference/react/cache#cache
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 97

the preloading process, ensuring it only occurs when necessary and


thereby enhancing application performance.
The effective use of caching in the preload function demonstrates
a keen understanding of optimizing network interactions, ensuring
that data fetching is both efficient and necessary. This considera-
tion for performance leads us to another crucial aspect of building
web applications in Next.js: the client component strategy.

Client Component Strategy


Pushing client components to the leaf of the component tree,
particularly in a server-side rendering environment like Next.js, is a
strategic approach for optimizing web application performance. By
doing so, you ensure that most of the page is server-rendered, which
speeds up the initial load time. Interactive elements that require
client-side JavaScript are loaded only where necessary, making the
page interactive more quickly without overburdening the initial
load.
In the provided code snippets, we have two versions of the Friends
component. The first version marks the entire component as a
client component, while the second version pushes the client-side
logic down to the individual Friend component.

First Version - Entire Component as


Client-Side

The first Friends component is wrapped with use client, making


the entire component and all its children client-side only. This
means that even simple, non-interactive parts of this component
won’t render until the client-side JavaScript loads and executes,
which can delay the initial rendering and interactivity of the page.
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 98

Figure 74. components/friends.tsx

1 'use client';
2
3 async function Friends({ id }: { id: string }) {
4 const friends = await getFriends(id);
5
6 return (
7 <NextUIProvider>
8 <div>
9 <h2>Friends</h2>
10 {/**/}
11 </div>
12 </NextUIProvider>
13 );
14 }

Figure 75. The boundary is set to Friends


Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 99

Second Version - Client Logic at Leaf Node

The second approach optimizes performance by removing the


use client directive from the Friends component. Instead, it
delegates client-specific logic to the Friend component. This way,
the Friends component itself can be pre-rendered on the server,
and only the interactive parts within each Friend component are
handled on the client-side. This granular approach speeds up the
initial page load, as less JavaScript needs to be downloaded and
executed to render the initial view.
Figure 76. components/friends.tsx

1 async function Friends({ id }: { id: string }) {


2 const friends = await getFriends(id);
3
4 return (
5 <div>
6 <h2>Friends</h2>
7 <div>
8 {friends.map((user) => (
9 <Friend user={user} key={user.id} />
10 ))}
11 </div>
12 </div>
13 );
14 }

And we then move NextUIProvider usage into friend component.


Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 100

Figure 77. components/friend.tsx


1 "use client";
2
3 export const Friend = ({ user }: { user: User }) => {
4 return (
5 <NextUIProvider>
6 {/* popover code */}
7 </NextUIProvider>
8 );
9 };

Figure 78. The boundary is set to Friend

By applying the client component at the leaf node (Friend), you


enhance the user experience with faster initial load times and pro-
gressively enhance the page with interactive elements as needed.
This approach aligns well with modern web development best
practices, particularly in frameworks like Next.js, where balancing
server-side and client-side rendering is key to achieving optimal
performance and user experience.
In the final chapter our focus shifts to the concepts of lazy loading
and preloading within the context of Next.js. Here, we illustrate
Chapter 11: Lazy Load, Dynamic Import, and Preload in Next.js 101

how these methods enhance both performance and user experience.


By employing practical examples, the chapter provides a com-
prehensive exploration of dynamic importing and effective data
management in Next.js applications. We delve deeper into these
advanced techniques, demonstrating their practical application in
optimizing performance, improving data handling, and refining
user interactions in Next.js.

You might also like