Over the years, there has been an increased demand for straightforward ways to compose UIs using JavaScript. Frontend developers look for out-of-the-box solutions provided by many different libraries and frameworks. React’s popularity in this area has persevered for a long time now since its original release in 2013. This chapter will look at design patterns that are helpful in the React universe.
React, also referred to as React.js, is an open source JavaScript library designed by Facebook to build UIs or UI components. It is, of course, not the only UI library out there. Preact, Vue, Angular, Svelte, Lit, and many others are also great for composing interfaces from reusable elements. Given React’s popularity, however, we have chosen it for our discussion on design patterns for the current decade.
When frontend developers talk about code, it’s most often in the context of designing interfaces for the web. And the way we think of interface composition is in elements like buttons, lists, navigation, etc. React provides an optimized and simplified way of expressing interfaces in these elements. It also helps build complex and tricky interfaces by organizing your interface into three key concepts: components, props, and state.
Because React is composition-focused, it can perfectly map to the elements of your design system. So, designing for React rewards you for thinking in a modular way. It allows you to develop individual components before putting together a page or view so that you fully understand each component’s scope and purpose—a process referred to as componentization.
We will use the following terms frequently in this chapter. Let’s quickly look at what each of them means:
React library, created by Facebook in 2013
The react-dom
package providing DOM-specific methods for client and server rendering
Syntax extension to JavaScript
A new way to use state and other React features without writing a class
The library to develop cross-platform native apps with JavaScript
A web app that loads new content on the same page without a full page refresh/reload.
Before we discuss React design patterns, it would be helpful to understand some basic concepts used in React:
JSX is an extension to JavaScript that embeds template HTML in JS using XML-like syntax. It is meant to be transformed into valid JavaScript, though the semantics of that transformation are implementation-specific. JSX rose to popularity with the React library but has also seen other implementations.
Components are the building blocks of any React app. They are like JavaScript functions that accept arbitrary input (Props) and return React elements describing what should be displayed on the screen. Everything on screen in a React app is part of a component. Essentially, a React app is just components within components within components. So developers don’t build pages in React; they build components. Components let you split your UI into independent, reusable pieces. If you’re used to designing pages, thinking from a component perspective might seem like a significant change. But if you use a design system or style guide, this might be a smaller paradigm shift than it looks.
Props are a short form for properties, and they refer to the internal data of a component in React. They are written inside component calls and are passed into components. They also use the same syntax as HTML attributes, e.g., prop = value
. Two things worth remembering about props are: (1) we determine the value of a prop and use it as part of the blueprint before the component is built, and (2) the value of a prop will never change, i.e., props are read-only once passed into components. You access a prop by referencing it via the this.props
property that every component can access.
State is an object that holds information that may change over the component’s lifetime. It is the current snapshot of data stored in a component’s props. The data can change over time, so techniques to manage data changes become necessary to ensure the component looks the way engineers want it to, at just the right time—this is called state management.
In client-side rendering (CSR), the server renders only the bare bones HTML container for a page. The logic, data fetching, templating, and routing required to display content on the page are handled by JavaScript code that executes on the client. CSR became popular as a method of building SPAs. It helped to blur the difference between websites and installed applications and works best for highly interactive applications. With React by default, most of the application logic is executed on the client. It interacts with the server through API calls to fetch or save data.
SSR is one of the oldest methods of rendering web content. SSR generates the complete HTML for the page content to be rendered in response to a user request. The content may include data from a data store or external API. React can be rendered isomorphically, which means that it can function both on the browser and other platforms like the server. Thus, UI elements may be rendered on the server using React.
In a server-rendered application, HTML for the current navigation is generated on the server and sent to the client. Since the server generated the markup, the client can quickly parse this and display it on the screen. The JavaScript required to make the UI interactive is loaded after this. The event handlers that will make UI elements like buttons interactive get attached only once the JavaScript bundle is loaded and processed. This process is called hydration. React checks the current DOM nodes and hydrates them with the corresponding JavaScript.
Older documentation suggests using Create React App (CRA) to build a new client-only SPA for learning React. It is a CLI tool to create a scaffolding React app for bootstrapping a project. However, CRA offers a restricted development experience, which is too limiting for many modern web applications. React recommends using a production-grade React-powered framework such as Next.js or Remix to build new web apps or websites. These frameworks provide features that most apps and sites eventually need, such as static HTML generation, file-based routing, SPA navigations, and real client code.
React has evolved over the years. Different features introduced to the library gave rise to various ways of solving common problems. Here are some popular design patterns for React that we will look into in detail in the following sections:
We may often want to use the same logic in multiple components within our application. This logic can include applying specific styling to components, requiring authorization, or adding a global state. One way to reuse the same logic in multiple components is by using the Higher-Order Component (HOC) pattern. This pattern allows us to reuse component logic throughout our application.
An HOC is a component that receives another component. The HOC can contain a specific feature that can be applied to a component we pass to it as a parameter. The HOC returns the component with the additional feature applied to it.
Say that we always wanted to add particular styling to multiple components in our application. Instead of creating a style object locally each time, we can create an HOC that adds the style objects to the component it received as a parameter:
function
withStyles
(
Component
)
{
return
props
=>
{
const
style
=
{
padding
:
'0.2rem'
,
margin
:
'1rem'
}
return
<
Component
style
=
{
style
}
{...
props
}
/>
}
}
const
Button
=
()
=
<
button
>
Click
me
!<
/button>
const
Text
=
()
=>
<
p
>
Hello
World
!<
/p>
const
StyledButton
=
withStyles
(
Button
)
const
StyledText
=
withStyles
(
Text
)
We just created a StyledButton
and StyledText
component, the modified versions of the Button
and Text
component. They now both contain the style that got added in the withStyles
HOC.
To take it further, let us look at an application that renders a list of dog images fetched from an API. When fetching the data, we want to show the user a “Loading…” screen. Instead of adding it to the DogImages
component directly, we can use an HOC that adds this logic.
Let’s create an HOC called withLoader
. An HOC should receive a component and return that component. In this case, the withLoader
HOC should receive the element which should display Loading…
until the data is fetched. To make the withLoader
HOC very reusable, we won’t hardcode the Dog API URL in that component. Instead, we can pass the URL as an argument to the withLoader
HOC, so this loader can be used on any component that needs a loading indicator while fetching data from a different API endpoint:
function
withLoader
(
Element
,
url
)
{
return
props
=>
{};
}
An HOC returns an element, a functional component props ⇒ {}
in this case, to which we want to add the logic that allows us to display a text with Loading…
as the data is still being fetched. Once the data has been fetched, the component should pass the fetched data as a prop. The complete code for withLoader
looks like this:
import
React
,
{
useEffect
,
useState
}
from
"react"
;
export
default
function
withLoader
(
Element
,
url
)
{
return
(
props
)
=>
{
const
[
data
,
setData
]
=
useState
(
null
);
useEffect
(()
=>
{
async
function
getData
()
{
const
res
=
await
fetch
(
url
);
const
data
=
await
res
.
json
();
setData
(
data
);
}
getData
();
},
[]);
if
(
!
data
)
{
return
<
div
>
Loading
...
<
/div>;
}
return
<
Element
{...
props
}
data
=
{
data
}
/>
;
};
}
We just created an HOC that can receive any component and URL:
In the useEffect
hook, the withLoader
HOC fetches the data from the API endpoint that we pass as the value of url
. While the data is being fetched, we return the element containing the Loading…
text.
Once the data has been fetched, we set data equal to the data that has been fetched. Since data is no longer null
, we can display the element that we passed to the HOC.
Now to show the Loading…
indicator on the DogImages
list, we will export the
“wrapped” withLoading
HOC around the DogImages
component. The withLoader
HOC also expects the url
to know which endpoint to fetch the data from. In this case, we want to add the Dog API endpoint. Since the withLoader
HOC returned the element with an extra data prop, DogImages
in this case, we can access the data prop in the DogImages
component:
import
React
from
"react"
;
import
withLoader
from
"./withLoader"
;
function
DogImages
(
props
)
{
return
props
.
data
.
message
.
map
((
dog
,
index
)
=>
(
<
img
src
=
{
dog
}
alt
=
"Dog"
key
=
{
index
}
/>
));
}
export
default
withLoader
(
DogImages
,
"https://dog.ceo/api/breed/labrador/images/random/6"
);
The HOC pattern allows us to provide the same logic to multiple components while keeping all the logic in one place. The withLoader
HOC doesn’t care about the component or URL it receives; as long as it’s a valid component and a valid API endpoint, it’ll simply pass the data from that API endpoint to the component that we pass.
We can also compose multiple HOCs. Let’s say we also want to add functionality that shows a hovering text box when the user hovers over the DogImages
list.
We must create a HOC that provides a hovering prop to the element we pass. Based on that prop, we can conditionally render the text box based on whether the user is hovering over the DogImages
list.
We can now wrap the withHover
HOC around the withLoader
HOC:
export
default
withHover
(
withLoader
(
DogImages
,
"https://dog.ceo/api/breed/labrador/images/random/6"
)
);
The DogImages
element now contains all props we passed from withHover
and
withLoader
.
We can also use the Hooks pattern to achieve similar results in some cases. We will discuss this pattern in detail later in this chapter, but for now, let’s just say that using Hooks can reduce the depth of the component tree, while using the HOC pattern, it’s easy to end up with a deeply nested component tree. The best use cases for HOC are those where the following are true:
The same uncustomized behavior needs to be used by many components throughout the application.
The component can work standalone without the added custom logic.
Using the HOC pattern allows us to keep logic we want to reuse all in one place. This reduces the risk of accidentally spreading bugs throughout the application by duplicating code repeatedly, potentially introducing new bugs. By keeping the logic in one place, we can keep our code DRY and efficiently enforce the separation of concerns.
The prop’s name that a HOC can pass to an element can cause a naming collision. For example:
function
withStyles
(
Component
)
{
return
props
=>
{
const
style
=
{
padding
:
'0.2rem'
,
margin
:
'1rem'
}
return
<
Component
style
=
{
style
}
{...
props
}
/>
}
}
const
Button
=
()
=
<
button
style
=
{{
color
:
'red'
}}
>
Click
me
!<
/button>
const
StyledButton
=
withStyles
(
Button
)
In this case, the withStyles
HOC adds a prop called style
to the element we pass to it. However, the Button
component already had a prop called style
, which will be overwritten! Make sure the HOC can handle accidental name collision by either renaming or merging the props:
function
withStyles
(
Component
)
{
return
props
=>
{
const
style
=
{
padding
:
'0.2rem'
,
margin
:
'1rem'
,
...
props
.
style
}
return
<
Component
style
=
{
style
}
{...
props
}
/>
}
}
const
Button
=
()
=
<
button
style
=
{{
color
:
'red'
}}
>
Click
me
!<
/button>
const
StyledButton
=
withStyles
(
Button
)
When using multiple composed HOCs that all pass props to the element wrapped within them, it can be challenging to figure out which HOC is responsible for which prop. This can hinder debugging and scaling an application easily.
In the section on HOCs, we saw that reusing component logic can be convenient if multiple components need access to the same data or contain the same logic.
Another way of making components reusable is by using the Render Prop pattern. A render prop is a prop on a component whose value is a function that returns a JSX element. The component itself does not render anything besides the render prop. It simply calls the render prop instead of implementing its own rendering logic.
Imagine that we have a Title
component that should just render the value we pass. We can use a render prop for this. Let’s pass the value we want the Title
component to render to the render prop:
<
Title
render
=
{()
=
>
<
h1
>
I am a render prop!</
h1
>
} />
We can render this data within the Title
component by returning the invoked render prop:
const
Title
=
props
=>
props
.
render
();
We have to pass a prop called render
to the Component element, which is a function that returns a React element:
import
React
from
"react"
;
import
{
render
}
from
"react-dom"
;
import
"./styles.css"
;
const
Title
=
(
props
)
=>
props
.
render
();
render
(
<
div
className
=
"App"
>
<
Title
render
=
{()
=>
(
<
h1
>
<
span
role
=
"img"
aria
-
label
=
"emoji"
>
✨
<
/span>
I
am
a
render
prop
!
{
" "
}
<
span
role
=
"img"
aria
-
label
=
"emoji"
>
✨
<
/span>
<
/h1>
)}
/>
<
/div>,
document
.
getElementById
(
"root"
)
);
The cool thing about render props is that the component receiving the prop is reusable. We can use it multiple times, passing different values to the render prop each time.
Although they’re called render props, a render prop doesn’t have to be called render. Any prop that renders JSX is considered a render prop. Thus we have three render props in the following example:
const
Title
=
(
props
)
=>
(
<>
{
props
.
renderFirstComponent
()}
{
props
.
renderSecondComponent
()}
{
props
.
renderThirdComponent
()}
<
/>
);
render
(
<
div
className
=
"App"
>
<
Title
renderFirstComponent
=
{()
=>
<
h1
>
First
render
prop
!<
/h1>}
renderSecondComponent
=
{()
=>
<
h2
>
Second
render
prop
!<
/h2>}
renderThirdComponent
=
{()
=>
<
h3
>
Third
render
prop
!<
/h3>}
/>
<
/div>,
document
.
getElementById
(
"root"
)
);
We’ve just seen that we can use render props to make a component reusable, as we can pass different data to the render prop each time.
A component that takes a render prop usually does much more than simply invoking the render prop. Instead, we typically want to pass data from the component that takes the render prop to the element we pass as a render prop:
function
Component
(
props
)
{
const
data
=
{
...
}
return
props
.
render
(
data
)
}
The render prop can now receive this value that we passed as its argument:
<
Component
render
=
{data
=
>
<
ChildComponent
data
=
{data}
/>
}
Before we look at another use case for the Render Props pattern, let’s understand the concept of “Lifting state up” in React.
Let’s say we have a temperature converter where you can provide the input in Celsius in one stateful input element. The corresponding Fahrenheit
and Kelvin
values are reflected instantly in two other components. For the input element to be able to share its state with other components, we will have to move the state up to the closest common ancestor of the components that need it. This is called “Lifting state up”:
function
Input
({
value
,
handleChange
})
{
return
<
input
value
=
{
value
}
onChange
=
{
e
=>
handleChange
(
e
.
target
.
value
)}
/>
;
}
function
Kelvin
({
value
=
0
})
{
return
<
div
className
=
"temp"
>
{
value
+
273.15
}
K
<
/div>;
}
function
Fahrenheit
({
value
=
0
})
{
return
<
div
className
=
"temp"
>
{(
value
*
9
)
/
5
+
32
}
°
F
<
/div>;
}
export
default
function
App
()
{
const
[
value
,
setValue
]
=
useState
(
""
);
return
(
<
div
className
=
"App"
>
<
h1
>
Temperature
Converter
<
/h1>
<
Input
value
=
{
value
}
handleChange
=
{
setValue
}
/>
<
Kelvin
value
=
{
value
}
/>
<
Fahrenheit
value
=
{
value
}
/>
<
/div>
);
}
Lifting state is a valuable React state management pattern because sometimes we want a component to be able to share its state with sibling components. In the case of small applications with a few components, we can avoid using a state management library like Redux or React Context and use this pattern instead to lift the state up to the closest common ancestor.
Although this is a valid solution, lifting the state in larger applications with components that handle many children can be tricky. Each state change could cause a re-render of all the children, even those that don’t handle the data, which could negatively affect your app’s performance. We can use the Render Props pattern to work around this problem. We will change the Input
component such that it can receive render props:
function
Input
(
props
)
{
const
[
value
,
setValue
]
=
useState
(
""
);
return
(
<>
<
input
type
=
"text"
value
=
{
value
}
onChange
=
{
e
=>
setValue
(
e
.
target
.
value
)}
placeholder
=
"Temp in °C"
/>
{
props
.
render
(
value
)}
<
/>
);
}
export
default
function
App
()
{
return
(
<
div
className
=
"App"
>
<
h1
>
Temperature
Converter
<
/h1>
<
Input
render
=
{
value
=>
(
<>
<
Kelvin
value
=
{
value
}
/>
<
Fahrenheit
value
=
{
value
}
/>
<
/>
)}
/>
<
/div>
);
}
Besides regular JSX components, we can pass functions as children to React components. This function is available to us through the children prop, which is technically also a render prop.
Let’s change the Input
component. Instead of explicitly passing the render prop, we’ll pass a function as a child for the Input
component:
export
default
function
App
()
{
return
(
<
div
className
=
"App"
>
<
h1
>
Temperature
Converter
<
/h1>
<
Input
>
{
value
=>
(
<>
<
Kelvin
value
=
{
value
}
/>
<
Fahrenheit
value
=
{
value
}
/>
<
/>
)}
<
/Input>
<
/div>
);
}
We have access to this function through the props.children
prop that’s available on the Input
component. Instead of calling props.render
with the user input value, we’ll call props.children
with the value of the user input:
function
Input
(
props
)
{
const
[
value
,
setValue
]
=
useState
(
""
);
return
(
<>
<
input
type
=
"text"
value
=
{
value
}
onChange
=
{
e
=>
setValue
(
e
.
target
.
value
)}
placeholder
=
"Temp in °C"
/>
{
props
.
children
(
value
)}
<
/>
);
}
This way, the Kelvin
and Fahrenheit
components have access to the value without worrying about the name of the render prop.
Sharing logic and data among several components is straightforward with the Render Props pattern. Components can be made reusable by using a render or children prop. Although the HOC pattern mainly solves the same issues, namely reusability and sharing data, the Render Props pattern solves some problems we could encounter using the HOC pattern.
The issue of naming collisions that we can run into by using the HOC pattern no longer applies by using the Render Props pattern since we don’t automatically merge props. We explicitly pass the props down to the child components with the value provided by the parent component.
Since we explicitly pass props, we solve the HOC’s implicit props issue. The props that should get passed down to the element are all visible in the render prop’s arguments list. This way, we know exactly where specific props come from. We can separate our app’s logic from rendering components through render props. The stateful component that receives a render prop can pass the data onto stateless components, which merely render the data.
React Hooks have mostly resolved the issues we tried to solve with render props. As Hooks changed how we can add reusability and data sharing to components, they can replace the Render Props pattern in many cases.
Since we can’t add lifecycle methods to a render prop, we can use it only on components that don’t need to alter the data they receive.
React 16.8 introduced a new feature called Hooks. Hooks make it possible to use React state and lifecycle methods without using an ES2015 class component. Although Hooks is not necessarily a design pattern, Hooks plays a vital role in your application design. Hooks can replace many traditional design patterns.
Let’s see how class components enabled the addition of state and lifecycle methods.
Before Hooks were introduced in React, we had to use class components to add state and lifecycle methods to components. A typical class component in React can look something like this:
class
MyComponent
extends
React
.
Component
{
// Adding state and binding custom methods
constructor
()
{
super
()
this
.
state
=
{
...
}
this
.
customMethodOne
=
this
.
customMethodOne
.
bind
(
this
)
this
.
customMethodTwo
=
this
.
customMethodTwo
.
bind
(
this
)
}
// Lifecycle Methods
componentDidMount
()
{
...}
componentWillUnmount
()
{
...
}
// Custom methods
customMethodOne
()
{
...
}
customMethodTwo
()
{
...
}
render
()
{
return
{
...
}}
}
A class component can contain the following:
A state in its constructor
Lifecycle methods such as componentDidMount
and componentWillUnmount
to perform side effects based on a component’s lifecycle
Custom methods to add extra logic to a class
Although we can still use class components after the introduction of React Hooks, using class components can have some downsides. For example, consider the following example where a simple div
functions as a button:
function
Button
()
{
return
<
div
className
=
"btn"
>
disabled
<
/div>;
}
Instead of always displaying disabled, we want to change it to enabled and add some extra CSS styling to the button when the user clicks on it. To do that, we need to add state to the component to know whether the status is enabled or disabled. This means we’d have to refactor the functional component entirely and make it a class component that keeps track of the button’s state:
export
default
class
Button
extends
React
.
Component
{
constructor
()
{
super
();
this
.
state
=
{
enabled
:
false
};
}
render
()
{
const
{
enabled
}
=
this
.
state
;
const
btnText
=
enabled
?
"enabled"
:
"disabled"
;
return
(
<
div
className
=
{
`btn enabled-
${
enabled
}
`
}
onClick
=
{()
=>
this
.
setState
({
enabled
:
!
enabled
})}
>
{
btnText
}
<
/div>
);
}
}
In this example, the component is minimal, and refactoring didn’t need much effort. However, your real-life components probably consist of many more lines of code, making refactoring the component much more difficult.
Besides ensuring you don’t accidentally change any behavior while refactoring the component, you must also understand how ES2015+ classes work. Knowing how to refactor a component properly without accidentally changing the data flow can be challenging.
The standard way to share code among several components is using the HOC or Render Props pattern. Although both patterns are valid and using them is a good practice, adding those patterns at a later point in time requires you to restructure your application.
Besides restructuring your app, which is trickier the bigger your components are, having many wrapping components to share code among deeper nested components can lead to something that’s best referred to as a “wrapper hell.” It’s not uncommon to open your dev tools and see a structure similar to the following:
<
WrapperOne
>
<
WrapperTwo
>
<
WrapperThree
>
<
WrapperFour
>
<
WrapperFive
>
<
Component
>
<
h1
>
Finally in the component!</
h1
>
</
Component
>
</
WrapperFive
>
</
WrapperFour
>
</
WrapperThree
>
</
WrapperTwo
>
</
WrapperOne
>
The wrapper hell can make it difficult to understand how data flows through your application, making it harder to figure out why unexpected behavior is happening.
As we add more logic to class components, the size of the component increases fast. Logic within that component can get tangled and unstructured, making it difficult for developers to understand where certain logic is used in the class component. This can make debugging and optimizing performance more difficult. Lifecycle methods also require quite a lot of duplication in the code.
Class components aren’t always a great feature in React. To solve the common issues that React developers can run into when using class components, React introduced React Hooks. React Hooks are functions you can use to manage a component’s state and lifecycle methods. React Hooks make it possible to:
Add state to a functional component
Manage a component’s lifecycle without having to use lifecycle methods such as componentDidMount
and componentWillUnmount
Reuse the same stateful logic among multiple components throughout the app
First, let’s look at how we can add state to a functional component using React Hooks.
React provides a Hook called useState
that manages state within a functional
component.
Let’s see how a class component can be restructured into a functional component using the useState
Hook. We have a class component called Input
, which renders an input field. The value of input in the state updates whenever the user types anything in the input field:
class
Input
extends
React
.
Component
{
constructor
()
{
super
();
this
.
state
=
{
input
:
""
};
this
.
handleInput
=
this
.
handleInput
.
bind
(
this
);
}
handleInput
(
e
)
{
this
.
setState
({
input
:
e
.
target
.
value
});
}
render
()
{
<
input
onChange
=
{
handleInput
}
value
=
{
this
.
state
.
input
}
/>
;
}
}
To use the useState
Hook, we need to access React’s useState
method. The
useState
method expects an argument: this is the initial value of the state, an empty string in this case.
We can destructure two values from the useState
method:
The current value of the state
The method with which we can update the state:
const
[
value
,
setValue
]
=
React
.
useState
(
initialValue
);
You can compare the first value to a class component’s this.state.[value]
. The second value can be compared to a class component’s this.setState
method.
Since we’re dealing with the value of an input, let’s call the current value of the state input and the method to update the state setInput
. The initial value should be an empty string:
const
[
input
,
setInput
]
=
React
.
useState
(
""
);
We can now refactor the Input
class component into a stateful functional
component:
function
Input
()
{
const
[
input
,
setInput
]
=
React
.
useState
(
""
);
return
<
input
onChange
=
{(
e
)
=>
setInput
(
e
.
target
.
value
)}
value
=
{
input
}
/>
;
}
The input field’s value is equal to the current value of the input state, just like in the class component example. When the user types in the input field, the value of the input state updates accordingly using the setInput
method:
import
React
,
{
useState
}
from
"react"
;
export
default
function
Input
()
{
const
[
input
,
setInput
]
=
useState
(
""
);
return
(
<
input
onChange
=
{
e
=>
setInput
(
e
.
target
.
value
)}
value
=
{
input
}
placeholder
=
"Type something..."
/>
);
}
We’ve seen we can use the useState
component to handle state within a functional component. Still, another benefit of class components was the possibility of adding lifecycle methods to a component.
With the useEffect
Hook, we can “hook into” a component’s lifecycle. The
useEffect
Hook effectively combines the componentDidMount
, componentDidUpdate
, and
componentWillUnmount
lifecycle methods:
componentDidMount
()
{
...
}
useEffect
(()
=>
{
...
},
[])
componentWillUnmount
()
{
...
}
useEffect
(()
=>
{
return
()
=>
{
...
}
},
[])
componentDidUpdate
()
{
...
}
useEffect
(()
=>
{
...
})
Let’s use the input example we used in the state Hook section. Whenever the user is typing anything in the input field, the value should also be logged to the console.
We need a useEffect
Hook that “listens” to the input value. We can do so by adding input to the dependency array of the useEffect
Hook. The dependency array is the second argument that the useEffect
Hook receives:
import
React
,
{
useState
,
useEffect
}
from
"react"
;
export
default
function
Input
()
{
const
[
input
,
setInput
]
=
useState
(
""
);
useEffect
(()
=>
{
console
.
log
(
`The user typed
${
input
}
`
);
},
[
input
]);
return
(
<
input
onChange
=
{
e
=>
setInput
(
e
.
target
.
value
)}
value
=
{
input
}
placeholder
=
"Type something..."
/>
);
}
The value of the input now gets logged to the console whenever the user types a value.
Besides the built-in Hooks that React provides (useState
, useEffect
, useReducer
, useRef
, useContext
, useMemo
, useImperativeHandle
, useLayoutEffect
, useDebugValue
, useCallback
), we can easily create our own custom Hooks.
You may have noticed that all Hooks start with “use.” It’s essential to begin your Hooks with “use” for React to check if it violates the rules of Hooks.
Let’s say we want to keep track of specific keys the user may press when writing the input. Our custom Hook should be able to receive the key we want to target as its argument.
We want to add a keydown
and keyup
event listener to the key that the user passed as an argument. If the user presses that key, meaning the keydown
event gets triggered, the state within the Hook should toggle to true. Otherwise, when the user stops pressing that button, the keyup
event gets triggered and the state toggles to false:
function
useKeyPress
(
targetKey
)
{
const
[
keyPressed
,
setKeyPressed
]
=
React
.
useState
(
false
);
function
handleDown
({
key
})
{
if
(
key
===
targetKey
)
{
setKeyPressed
(
true
);
}
}
function
handleUp
({
key
})
{
if
(
key
===
targetKey
)
{
setKeyPressed
(
false
);
}
}
React
.
useEffect
(()
=>
{
window
.
addEventListener
(
"keydown"
,
handleDown
);
window
.
addEventListener
(
"keyup"
,
handleUp
);
return
()
=>
{
window
.
removeEventListener
(
"keydown"
,
handleDown
);
window
.
removeEventListener
(
"keyup"
,
handleUp
);
};
},
[]);
return
keyPressed
;
}
We can use this custom Hook in our input application. Let’s log to the console whenever the user presses the q, l, or w key:
import
React
from
"react"
;
import
useKeyPress
from
"./useKeyPress"
;
export
default
function
Input
()
{
const
[
input
,
setInput
]
=
React
.
useState
(
""
);
const
pressQ
=
useKeyPress
(
"q"
);
const
pressW
=
useKeyPress
(
"w"
);
const
pressL
=
useKeyPress
(
"l"
);
React
.
useEffect
(()
=>
{
console
.
log
(
`The user pressed Q!`
);
},
[
pressQ
]);
React
.
useEffect
(()
=>
{
console
.
log
(
`The user pressed W!`
);
},
[
pressW
]);
React
.
useEffect
(()
=>
{
console
.
log
(
`The user pressed L!`
);
},
[
pressL
]);
return
(
<
input
onChange
=
{
e
=>
setInput
(
e
.
target
.
value
)}
value
=
{
input
}
placeholder
=
"Type something..."
/>
);
}
Instead of keeping the key press logic local to the Input
component, we can reuse the useKeyPress
Hook with multiple components without having to rewrite the same code repeatedly.
Another great advantage of Hooks is that the community can build and share Hooks. We wrote the useKeyPress
Hook ourselves, but that wasn’t necessary. The Hook was already built by someone else and ready to use in our application if we had just installed it.
The following are some websites that list all the Hooks built by the community and ready to use in your application:
Like other components, special functions are used when you want to add Hooks to the code you have written. Here’s a brief overview of some common Hook functions:
useState
The useState
Hook enables developers to update and manipulate state inside function components without needing to convert it to a class component. One advantage of this Hook is that it is simple and does not require as much complexity as other React Hooks.
useEffect
The useEffect
Hook is used to run code during major lifecycle events in a function component. The main body of a function component does not allow mutations, subscriptions, timers, logging, and other side effects. It could lead to confusing bugs and inconsistencies within the UI if they are allowed.
The useEffect
Hook prevents all of these “side effects” and allows the UI
to run smoothly. It combines componentDidMount
, componentDidUpdate
, and
componentWillUnmount
, all in one place.
useContext
The useContext
Hook accepts a context object, the value returned from React.createcontext
, and returns the current context value for that context. The useContext
Hook also works with the React Context API to share data throughout the app without passing your app props down through various levels. Note that the argument passed to the useContext
Hook must be the context object itself and any component calling the useContext
always rerenders whenever the context value changes.
useReducer
The useReducer
Hook gives an alternative to setState
. It is especially preferable when you have complex state logic that involves multiple subvalues or when the next state depends on the previous one. It takes on a reducer function and an initial state input and returns the current state and a dispatch function as output using array destructuring. useReducer
also optimizes the performance of components that trigger deep updates.
Here are some benefits of making use of Hooks:
Hooks allow you to group code by concern and functionality, not by lifecycle. This makes the code not only cleaner and concise but also shorter. What follows is a comparison of a simple stateful component of a searchable product data table using React and how it looks in Hooks after using the useState
keyword.
Stateful component:
class
TweetSearchResults
extends
React
.
Component
{
constructor
(
props
)
{
super
(
props
);
this
.
state
=
{
filterText
:
''
,
inThisLocation
:
false
};
this
.
handleFilterTextChange
=
this
.
handleFilterTextChange
.
bind
(
this
);
this
.
handleInThisLocationChange
=
this
.
handleInThisLocationChange
.
bind
(
this
);
}
handleFilterTextChange
(
filterText
)
{
this
.
setState
({
filterText
:
filterText
});
}
handleInThisLocationChange
(
inThisLocation
)
{
this
.
setState
({
inThisLocation
:
inThisLocation
})
}
render
()
{
return
(
<
div
>
<
SearchBar
filterText
=
{
this
.
state
.
filterText
}
inThisLocation
=
{
this
.
state
.
inThisLocation
}
onFilterTextChange
=
{
this
.
handleFilterTextChange
}
onInThisLocationChange
=
{
this
.
handleInThisLocationChange
}
/>
<
TweetList
tweets
=
{
this
.
props
.
tweets
}
filterText
=
{
this
.
state
.
filterText
}
inThisLocation
=
{
this
.
state
.
inThisLocation
}
/>
<
/div>
);
}
}
Here’s the same component with Hooks:
const
TweetSearchResults
=
({
tweets
})
=>
{
const
[
filterText
,
setFilterText
]
=
useState
(
''
);
const
[
inThisLocation
,
setInThisLocation
]
=
useState
(
false
);
return
(
<
div
>
<
SearchBar
filterText
=
{
filterText
}
inThisLocation
=
{
inThisLocation
}
setFilterText
=
{
setFilterText
}
setInThisLocation
=
{
setInThisLocation
}
/>
<
TweetList
tweets
=
{
tweets
}
filterText
=
{
filterText
}
inThisLocation
=
{
inThisLocation
}
/>
<
/div>
);
}
JavaScript classes can be challenging to manage, can be hard to use with hot reloading, and may need to be minified better. React Hooks solves these problems and ensures functional programming is made easy. With the implementation of Hooks, we don’t need to have class components.
Classes in JavaScript encourage multiple levels of inheritance that quickly increase overall complexity and potential for errors. However, Hooks allow you to use state and other React features without writing a class. With React, you can always reuse stateful logic without needing to rewrite the code repeatedly. This reduces the chances of errors and allows for composition with plain functions.
Until the implementation of Hooks, React had no way of extracting and sharing nonvisual logic. This eventually led to more complexities, such as the HOC patterns and Render Props, to solve a common problem. The introduction of Hooks has solved this problem because it allows for extracting stateful logic to a simple JavaScript function.
There are, of course, some potential downsides to Hooks worth keeping in mind:
Have to respect its rules. With a linter plug-in, knowing which rule has been broken is easier.
Need considerable time practicing to use correctly (e.g., useEffect
).
Need to be aware of the wrong use (e.g., useCallback
, useMemo
).
When Hooks were introduced to React, it created a new problem: how do we know when to use function components with Hooks and class components? With the help of Hooks, it is possible to get state and partial lifecycle Hooks even in function components. Hooks allow you to use local state and other React features without writing a class. Here are some differences between Hooks and classes to help you decide:
Hooks help avoid multiple hierarchies and make the code clearer. With classes, generally, when you use HOC or Render Props, you have to restructure your app with multiple hierarchies when you try to see it in DevTools.
Hooks provide uniformity across React components. Classes confuse humans and machines due to the need to understand binding and the context in which functions are called.
The import
keyword allows us to import code that another module has exported. By default, all modules we’re statically importing get added to the initial bundle. A module imported using the default ES2015+ import syntax, import module from
[module]
, is statically imported. In this section, we will learn the use of static imports in the React.js context.
Let’s look at an example. A simple chat app contains a Chat
component, in
which we are statically importing and rendering three components: UserProfile
, a ChatList
, and a ChatInput
to type and send messages. Within the ChatInput
module, we’re statically importing an EmojiPicker
component to show the emoji picker when the user toggles the emoji. We will use webpack to bundle our module
dependencies:
import
React
from
"react"
;
// Statically import Chatlist, ChatInput and UserInfo
import
UserInfo
from
"./components/UserInfo"
;
import
ChatList
from
"./components/ChatList"
;
import
ChatInput
from
"./components/ChatInput"
;
import
"./styles.css"
;
console
.
log
(
"App loading"
,
Date
.
now
());
const
App
=
()
=>
(
<
div
className
=
"App"
>
<
UserInfo
/>
<
ChatList
/>
<
ChatInput
/>
<
/div>
);
export
default
App
;
The modules get executed as soon as the engine reaches the line on which we import them. When you open the console, you can see the order in which the modules were loaded.
Since the components were statically imported, webpack bundled the modules into the initial bundle. We can see the bundle that webpack creates after building the application:
Asset |
main.bundle.js |
Size |
1.5 MiB |
Chunks |
main [emitted] |
Chunk Names |
main |
Our chat application’s source code gets bundled into one bundle: main.bundle.js. A large bundle size can significantly affect our application’s loading time depending on the user’s device and network connection. Before the App
component can render its contents to the user’s screen, it must first load and parse all modules.
Luckily, there are many ways to speed up the loading time! We don’t always have to import all modules at once: there may be modules that should only get rendered based on user interaction, like the EmojiPicker in this case, or rendered further down the page. Instead of importing all components statically, we can dynamically import the modules after the App
component has rendered its contents, and the user can interact with our application.
The chat application discussed in the previous section on Static Imports had four key components: UserInfo
, ChatList
, ChatInput
, and EmojiPicker
. However, only three of these components are used instantly on the initial page load: UserInfo
, ChatList
, and ChatInput
. The EmojiPicker isn’t directly visible and may only be rendered if the user clicks on the Emoji to toggle the EmojiPicker. This implies that we unnecessarily added the EmojiPicker module to our initial bundle, potentially increasing the loading time.
To solve this, we can dynamically import the EmojiPicker
component. Instead of statically importing it, we’ll import it only when we want to show the EmojiPicker. An easy way to dynamically import components in React is by using React Suspense. The React.Suspense
component receives the component that should be dynamically loaded, making it possible for the App
component to render its contents faster by suspending the import of the EmojiPicker module. When the user clicks on the Emoji, the EmojiPicker
component gets rendered for the first time. The EmojiPicker
component renders a Suspense
component, which receives the lazily imported module: the EmojiPicker in this case. The Suspense
component accepts a fallback prop, which receives the component that should get rendered while the suspended component is still loading!
Instead of unnecessarily adding EmojiPicker to the initial bundle, we can split it up into its own bundle and reduce the size of the initial bundle.
A smaller initial bundle size means a faster initial load: the user doesn’t have to stare at a blank loading screen for as long. The fallback component lets the user know that our application hasn’t frozen: they need to wait a little while for the module to be processed and executed.
Asset | Size | Chunks | Chunk names |
---|---|---|---|
emoji-picker.bundle.js |
1.48 KiB |
1 [emitted] |
emoji-picker |
main.bundle.js |
1.33 MiB |
main [emitted] |
main |
vendors~emoji-picker.bundle.js |
171 KiB |
2 [emitted] |
vendors~emoji-picker |
Whereas previously, the initial bundle was 1.5 MiB, we’ve reduced it to 1.33 MiB by suspending the import of the EmojiPicker. In the console, you can see that the EmojiPicker gets executed once we’ve toggled the EmojiPicker:
import
React
,
{
Suspense
,
lazy
}
from
"react"
;
// import Send from "./icons/Send";
// import Emoji from "./icons/Emoji";
const
Send
=
lazy
(()
=>
import
(
/*webpackChunkName: "send-icon" */
"./icons/Send"
)
);
const
Emoji
=
lazy
(()
=>
import
(
/*webpackChunkName: "emoji-icon" */
"./icons/Emoji"
)
);
// Lazy load EmojiPicker when <EmojiPicker /> renders
const
Picker
=
lazy
(()
=>
import
(
/*webpackChunkName: "emoji-picker" */
"./EmojiPicker"
)
);
const
ChatInput
=
()
=>
{
const
[
pickerOpen
,
togglePicker
]
=
React
.
useReducer
(
state
=>
!
state
,
false
);
return
(
<
Suspense
fallback
=
{
<
p
id
=
"loading"
>
Loading
...
<
/p>}>
<
div
className
=
"chat-input-container"
>
<
input
type
=
"text"
placeholder
=
"Type a message..."
/>
<
Emoji
onClick
=
{
togglePicker
}
/>
{
pickerOpen
&&
<
Picker
/>
}
<
Send
/>
<
/div>
<
/Suspense>
);
};
console
.
log
(
"ChatInput loaded"
,
Date
.
now
());
export
default
ChatInput
;
When building the application, we can see the different bundles that webpack created. By dynamically importing the EmojiPicker
component, we reduced the initial bundle size from 1.5 MiB to 1.33 MiB! Although the user may still have to wait a while until the EmojiPicker has been fully loaded, we have improved the UX by making sure the application is rendered and interactive while the user waits for the component to load.
SSR doesn’t support React Suspense (yet). An excellent alternative to React Suspense is the loadable-components library, which you can use in SSR applications:
import
React
from
"react"
;
import
loadable
from
"@loadable/component"
;
import
Send
from
"./icons/Send"
;
import
Emoji
from
"./icons/Emoji"
;
const
EmojiPicker
=
loadable
(()
=>
import
(
"./EmojiPicker"
),
{
fallback
:
<
div
id
=
"loading"
>
Loading
...
<
/div>
});
const
ChatInput
=
()
=>
{
const
[
pickerOpen
,
togglePicker
]
=
React
.
useReducer
(
state
=>
!
state
,
false
);
return
(
<
div
className
=
"chat-input-container"
>
<
input
type
=
"text"
placeholder
=
"Type a message..."
/>
<
Emoji
onClick
=
{
togglePicker
}
/>
{
pickerOpen
&&
<
EmojiPicker
/>
}
<
Send
/>
<
/div>
);
};
export
default
ChatInput
;
Like React Suspense, we can pass the lazily imported module to the loadable, which will import the module only once the EmojiPicker module is requested. While the module is loaded, we can render a fallback component.
Although loadable components are a great alternative to React Suspense for SSR applications, they’re also helpful in CSR applications to suspend module imports:
import
React
from
"react"
;
import
Send
from
"./icons/Send"
;
import
Emoji
from
"./icons/Emoji"
;
import
loadable
from
"@loadable/component"
;
const
EmojiPicker
=
loadable
(()
=>
import
(
"./components/EmojiPicker"
),
{
fallback
:
<
p
id
=
"loading"
>
Loading
...
<
/p>
});
const
ChatInput
=
()
=>
{
const
[
pickerOpen
,
togglePicker
]
=
React
.
useReducer
(
state
=>
!
state
,
false
);
return
(
<
div
className
=
"chat-input-container"
>
<
input
type
=
"text"
placeholder
=
"Type a message..."
/>
<
Emoji
onClick
=
{
togglePicker
}
/>
{
pickerOpen
&&
<
EmojiPicker
/>
}
<
Send
/>
<
/div>
);
};
console
.
log
(
"ChatInput loaded"
,
Date
.
now
());
export
default
ChatInput
;
Besides user interaction, we often have components that need not be visible on the initial page load. An excellent example of this is lazy-loading images or components that aren’t directly visible in the viewport and get loaded only once the user scrolls down. Triggering a dynamic import when the user scrolls down to a component and it becomes visible is called Import on Visibility.
To know whether components are currently in our viewport, we can use
the IntersectionObserver API or libraries such as
react-loadable-visibility
or
react-lazyload
to add Import on Visibility to our application quickly. We can now look at the chat application example where the EmojiPicker is imported and loaded when it becomes visible to the user:
import
React
from
"react"
;
import
Send
from
"./icons/Send"
;
import
Emoji
from
"./icons/Emoji"
;
import
LoadableVisibility
from
"react-loadable-visibility/react-loadable"
;
const
EmojiPicker
=
LoadableVisibility
({
loader
:
()
=>
import
(
"./EmojiPicker"
),
loading
:
<
p
id
=
"loading"
>
Loading
<
/p>
});
const
ChatInput
=
()
=>
{
const
[
pickerOpen
,
togglePicker
]
=
React
.
useReducer
(
state
=>
!
state
,
false
);
return
(
<
div
className
=
"chat-input-container"
>
<
input
type
=
"text"
placeholder
=
"Type a message..."
/>
<
Emoji
onClick
=
{
togglePicker
}
/>
{
pickerOpen
&&
<
EmojiPicker
/>
}
<
Send
/>
<
/div>
);
};
console
.
log
(
"ChatInput loading"
,
Date
.
now
());
export
default
ChatInput
;
In the previous section, we saw how we could dynamically import components when needed. In a complex application with multiple routes and components, we must ensure that our code is bundled and split optimally to allow for a mix of static and dynamic imports at the right time.
You can use the route-based splitting pattern to split your code or rely on modern bundlers such as webpack or Rollup to split and bundle your application’s source code.
Specific resources may be required only on certain pages or routes, and we can request resources that are needed only for particular routes by adding route-based splitting. By combining React Suspense or loadable-components with libraries such as react-router
, we can dynamically load components based on the current route. For example:
import
React
,
{
lazy
,
Suspense
}
from
"react"
;
import
{
render
}
from
"react-dom"
;
import
{
Switch
,
Route
,
BrowserRouter
as
Router
}
from
"react-router-dom"
;
const
App
=
lazy
(()
=>
import
(
/* webpackChunkName: "home" */
"./App"
));
const
Overview
=
lazy
(()
=>
import
(
/* webpackChunkName: "overview" */
"./Overview"
)
);
const
Settings
=
lazy
(()
=>
import
(
/* webpackChunkName: "settings" */
"./Settings"
)
);
render
(
<
Router
>
<
Suspense
fallback
=
{
<
div
>
Loading
...
<
/div>}>
<
Switch
>
<
Route
exact
path
=
"/"
>
<
App
/>
<
/Route>
<
Route
path
=
"/overview"
>
<
Overview
/>
<
/Route>
<
Route
path
=
"/settings"
>
<
Settings
/>
<
/Route>
<
/Switch>
<
/Suspense>
<
/Router>,
document
.
getElementById
(
"root"
)
);
module
.
hot
.
accept
();
By lazily loading the components per route, we’re only requesting the bundle that contains the code necessary for the current route. Since most people are used to the fact that there may be some loading time during a redirect, it’s the perfect place to load components lazily.
When building a modern web application, bundlers such as webpack or Rollup take an application’s source code and bundle this together into one or more bundles. When a user visits a website, the bundles required to display the data and features to the user are requested and loaded.
JavaScript engines such as V8 can parse and compile data requested by the user as it’s being loaded. Although modern browsers have evolved to parse and compile the code as quickly and efficiently as possible, the developer is still in charge of optimizing the loading and execution time of the requested data. We want to keep the execution time as short as possible to prevent blocking the main thread.
Even though modern browsers can stream the bundle as it arrives, it can still take a significant time before the first pixel is painted on the user’s device. The bigger the bundle, the longer it can take before the engine reaches the line on which the first rendering call is made. Until then, the user has to stare at a blank screen, which can be frustrating.
We want to display data to the user as quickly as possible. A larger bundle leads to an increased amount of loading time, processing time, and execution time. It would be great if we could reduce the size of this bundle to speed things up. Instead of requesting one giant bundle that contains unnecessary code, we can split the bundle into multiple smaller bundles. There are some essential metrics that we need to consider when deciding the size of our bundles.
By bundle-splitting the application, we can reduce the time it takes to load, process, and execute a bundle. This, in turn, reduces the time it takes for the first content to be painted on the user’s screen, known as the First Contentful Paint (FCP). It also reduces the time for the largest component to be rendered to the screen or the Largest Contentful Paint (LCP) metric.
Although seeing data on our screen is excellent, we want to see more than just the content. To have a fully functioning application, we want users to be able to interact with it as well. The UI becomes interactive only after the bundle has been loaded and executed. The time it takes for all content to be painted on the screen and become interactive is called the Time to Interactive (TTI).
A larger bundle doesn’t necessarily mean a longer execution time. It could happen that we loaded a ton of code that the user may not even use. Some parts of the bundle will get executed on only a specific user interaction, which the user may or may not do.
The engine still has to load, parse, and compile code that’s not even used on the initial render before the user can see anything on their screen. Although the parsing and compilation costs can be practically ignored due to the browser’s performant way of handling these two steps, fetching a larger bundle than necessary can hurt the performance of your application. Users on low-end devices or slower networks will see a significant increase in loading time before the bundle is fetched.
Instead of initially requesting parts of the code that don’t have a high priority in the current navigation, we can separate this code from the code needed to render the initial page.
Making applications accessible to a global user base can be a challenge. The application should be performant on low-end devices and in regions with poor internet connectivity. To make sure our application can load as efficiently as possible under challenging conditions, we can use the Push Render Pre-cache Lazy-load (PRPL) pattern.
The PRPL pattern focuses on four primary performance considerations:
Pushing critical resources efficiently minimizes the number of roundtrips to the server and reduces loading time.
Rendering the initial route as soon as possible to improve the UX.
Pre-caching assets in the background for frequently visited routes to minimize the number of requests to the server and enable a better offline experience.
Lazily loading routes or assets that aren’t requested as frequently.
When we visit a website, the browser requests the server for the required resources. The file the entry point points to gets returned from the server, usually our application’s initial HTML file. The browser’s HTML parser starts to parse this data as soon as it starts receiving it from the server. If the parser discovers that more resources are needed, such as stylesheets or scripts, additional HTTP requests are sent to the server to get those resources.
Requesting resources repeatedly isn’t optimal, as we are trying to minimize the number of round trips between the client and the server.
We used HTTP/1.1 for a long time to communicate between the client and the server. Although HTTP/1.1 introduced many improvements compared to HTTP/1.0, such as keeping the TCP connection between the client and the server alive before a new HTTP request gets sent with the keep-alive header, there were still some issues that had to be solved. HTTP/2 introduced significant changes compared to HTTP/1.1, allowing us to optimize the message exchange between the client and the server.
Whereas HTTP/1.1 used a newline delimited plaintext protocol in the requests and responses, HTTP/2 splits the requests and responses into smaller frames. An HTTP request that contains headers and a body field gets split into at least two frames: a headers frame and a data frame.
HTTP/1.1 had a maximum amount of six TCP connections between the client and the server. Before you can send a new request over the same TCP connection, the previous request must be resolved. If the last request takes a long time to resolve, this request is blocking the other requests from being sent. This common issue is called head-of-line blocking and can increase the loading time of specific resources.
HTTP/2 uses bidirectional streams. A single TCP connection with multiple bidirectional streams can carry multiple request and response frames between the client and the server. Once the server has received all request frames for that specific request, it reassembles them and generates response frames. These response frames are sent back to the client, which reassembles them. Since the stream is bidirectional, we can send both request and response frames over the same stream.
HTTP/2 solves head-of-line blocking by allowing multiple requests to get sent on the same TCP connection before the previous request resolves! HTTP/2 also introduced a more optimized way of fetching data, called server push. Instead of explicitly asking for resources each time by sending an HTTP request, the server can send the additional resources automatically by “pushing” these resources.
After the client has received the additional resources, the resources will get stored in the browser cache. When the resources are discovered while parsing the entry file, the browser can quickly get the resources from the cache instead of making an HTTP request to the server.
Although pushing resources reduces the time to receive additional resources, server push is not HTTP cache-aware. The pushed resources won’t be available to us the next time we visit the website and will have to be requested again. To solve this, the PRPL pattern uses service workers after the initial load to cache those resources to ensure the client isn’t making unnecessary requests.
As site authors, we usually know what resources are critical to fetch early on, while browsers do their best to guess this. We can help the browser by adding a preload resource hint to the critical resources.
By telling the browser that you’d like to preload a specific resource, you’re telling the browser that you would like to fetch it sooner than the browser would otherwise discover it. Preloading is a great way to optimize the time it takes to load resources critical for the current route.
Although preloading resources is a great way to reduce the number of roundtrips and optimize loading time, pushing too many files can be harmful. The browser’s cache is limited, and you may unnecessarily use bandwidth by requesting resources the client didn’t need. The PRPL pattern focuses on optimizing the initial load. No other resources get loaded before the initial route is completely rendered.
We can achieve this by code-splitting our application into small, performant bundles. These bundles should allow users to load only the resources they need when they need them while also maximizing cache use.
Caching larger bundles can be an issue. Multiple bundles may share the same resources.
A browser needs help identifying which parts of the bundle are shared between multiple routes and cannot cache these resources. Caching resources is vital to reducing the number of round trips to the server and to making our application offline-friendly.
When working with the PRPL pattern, we need to ensure that the bundles we’re requesting contain the minimal amount of resources we need at that time and are cachable by the browser. In some cases, this could mean that having no bundles at all would be more performant, and we could simply work with unbundled modules.
The benefit of dynamically requesting minimal resources by bundling an application can easily be mocked by configuring the browser and server to support HTTP/2 push and caching the resources efficiently. For browsers that don’t support HTTP/2 server push, we can create an optimized build to minimize the number of roundtrips. The client doesn’t have to know whether it’s receiving a bundled or unbundled resource: the server delivers the appropriate build for each browser.
The PRPL pattern often uses an app shell as its main entry point, a minimal file containing most of the application’s logic and shared between routes. It also includes the application’s router, which can dynamically request the necessary resources.
The PRPL pattern ensures that no other resources get requested or rendered before the initial route is visible on the user’s device. Once the initial route has been loaded successfully, a service worker can get installed to fetch the resources for the other frequently visited routes in the background.
Since this data is being fetched in the background, the user won’t experience any delays. If a user wants to navigate to a frequently visited route cached by the service worker, the service worker can quickly get the required resources from the cache instead of sending a request to the server.
Resources for routes that aren’t as frequently visited can be dynamically imported.
The Loading Prioritization pattern encourages you to explicitly prioritize the requests for specific resources you know will be required earlier.
Preload (<link rel="preload">
) is a browser optimization that allows critical resources (that browsers may discover late) to be requested earlier. If you are comfortable thinking about how to order the loading of your key resources manually, it can have a positive impact on loading performance and metrics in the Core Web Vitals (CWV). That said, preload is not a panacea and requires an awareness of some
trade-offs:
<
link
rel
=
"preload"
href
=
"emoji-picker.js"
as
=
"script"
>
...</
head
>
<
body
>
...<
script
src
=
"stickers.js"
defer
></
script
>
<
script
src
=
"video-sharing.js"
defer
></
script
>
<
script
src
=
"emoji-picker.js"
defer
></
script
>
When optimizing for metrics like Time to Interactive (TTI) or First Input Delay (FID), preload can be helpful to load JavaScript bundles (or chunks) necessary for interactivity. Remember that great care is needed when using preload because you want to avoid improving interactivity at the cost of delaying resources (like hero images or fonts) necessary for First Contentful Paint (FCP) or Largest Contentful Paint (LCP).
If you’re trying to optimize the loading of first-party JavaScript, consider using <script defer>
in the document <head>
versus <body>
to help with the early discovery of these resources.
While prefetching is a great way to cache resources that may be requested sometime soon, we can preload resources that need to be used instantly. It could be a specific font used on the initial render or certain images the user sees immediately.
Say our EmojiPicker
component should be visible instantly on the initial render. Although you should not include it in the main bundle, it should get loaded in parallel. Like prefetch, we can add a magic comment to inform webpack that this module should be preloaded:
const
EmojiPicker
=
import
(
/* webpackPreload: true */
"./EmojiPicker"
);
After building the application, we can see that the EmojiPicker
will be prefetched. The actual output is visible as a link
tag with rel="preload"
in the head of our document:
<
link
rel
=
"prefetch"
href
=
"emoji-picker.bundle.js"
as
=
"script"
/>
<
link
rel
=
"prefetch"
href
=
"vendors~emoji-picker.bundle.js"
as
=
"script"
/>
The preloaded EmojiPicker
could be loaded in parallel with the initial bundle. Unlike prefetch, where the browser still had a say in whether it’s got a good enough internet connection and bandwidth to prefetch the resource, a preloaded resource will get preloaded no matter what.
Instead of waiting until the EmojiPicker
gets loaded after the initial render, the resource will be available to us instantly. As we’re loading assets with more thoughtful ordering, the initial loading time may increase significantly depending on your user’s device and internet connection. Only preload the resources that must be visible approximately 1 second after the initial render.
Should you wish for browsers to download a script as high priority but not block the parser waiting for a script, you can take advantage of the preload + async
hack shown here. The preload, in this case, may delay the download of other resources, but this is a trade-off a developer has to make:
<
link
rel
=
"preload"
href
=
"emoji-picker.js"
as
=
"script"
/>
<
script
src
=
"emoji-picker.js"
async
></
script
>
The feature is slightly safer thanks to some fixes to preload’s queue-jumping behavior in Chrome 95+. Pat Meenan of Chrome’s new recommendations for preload suggests the following:
Putting it in HTTP headers will jump ahead of everything else.
Generally, preloads will load in the order the parser gets to them for anything greater than or equal to Medium, so be careful putting preloads at the beginning of the HTML.
Font preloads are probably best toward the end of the head or the beginning of the body.
Import preloads should be done after the script
tag that needs the import (so the actual script gets loaded/parsed first).
Image preloads will have a low priority and should be ordered relative to async
scripts and other low/lowest priority tags.
List virtualization helps improve the rendering performance of large lists of data. You render only visible rows of content in a dynamic list instead of the entire list. The rows rendered are only a small subset of the full list, with what is visible (the window) moving as the user scrolls. In React, you can achieve this using react-virtualized
. It’s a windowing library by Brian Vaughn that renders only the items currently visible in a list (within a scrolling viewport). This means you don’t need to pay the cost of thousands of rows of data being rendered at once.
Virtualizing a list of items involves maintaining and moving a window around your list. Windowing in react-virtualized
works by:
Having a small container DOM element (e.g., <ul>
) with relative positioning (window).
Having a big DOM element for scrolling.
Absolutely positioning children inside the container, setting their styles for top, left, width, and height.
Rather than rendering thousands of elements from a list at once (which can cause slower initial rendering or impact scroll performance), virtualization focuses on rendering just items visible to the user.
This can help keep list rendering fast on mid- to low-end devices. You can fetch/display more items as the user scrolls, unloading previous entries and replacing them with new ones.
react-window
is a rewrite of
react-virtualized
by the same author aiming to be smaller, faster, and more tree-shakeable. In a tree-shakeable library, size is a function of which API surfaces you choose to use. I’ve seen approximately 20 to 30 KB (gzipped) savings using it in place of react-virtualized
. The APIs for both packages are similar; react-window
tends to be simpler where they
differ.
The following are the main components in the case of react-window
.
Lists render a windowed list (row) of elements meaning that only the visible rows are displayed to users. Lists use a Grid (internally) to render rows, relaying props to that inner Grid. The following code snippets show the difference between rendering lists in React versus using react-window
.
Rendering a list of simple data (itemsArray
) using React:
import
React
from
"react"
;
import
ReactDOM
from
"react-dom"
;
const
itemsArray
=
[
{
name
:
"Drake"
},
{
name
:
"Halsey"
},
{
name
:
"Camila Cabello"
},
{
name
:
"Travis Scott"
},
{
name
:
"Bazzi"
},
{
name
:
"Flume"
},
{
name
:
"Nicki Minaj"
},
{
name
:
"Kodak Black"
},
{
name
:
"Tyga"
},
{
name
:
"Bruno Mars"
},
{
name
:
"Lil Wayne"
},
...
];
// our data
const
Row
=
({
index
,
style
})
=>
(
<
div
className
=
{
index
%
2
?
"ListItemOdd"
:
"ListItemEven"
}
style
=
{
style
}
>
{
itemsArray
[
index
].
name
}
<
/div>
);
const
Example
=
()
=>
(
<
div
style
=
class
=
"List"
>
{
itemsArray
.
map
((
item
,
index
)
=>
Row
({
index
}))}
<
/div>
);
ReactDOM
.
render
(
<
Example
/>
,
document
.
getElementById
(
"root"
));
Rendering a list using react-window
:
import
React
from
"react"
;
import
ReactDOM
from
"react-dom"
;
import
{
FixedSizeList
as
List
}
from
"react-window"
;
const
itemsArray
=
[...];
// our data
const
Row
=
({
index
,
style
})
=>
(
<
div
className
=
{
index
%
2
?
"ListItemOdd"
:
"ListItemEven"
}
style
=
{
style
}
>
{
itemsArray
[
index
].
name
}
<
/div>
);
const
Example
=
()
=>
(
<
List
className
=
"List"
height
=
{
150
}
itemCount
=
{
itemsArray
.
length
}
itemSize
=
{
35
}
width
=
{
300
}
>
{
Row
}
<
/List>
);
ReactDOM
.
render
(
<
Example
/>
,
document
.
getElementById
(
"root"
));
Grid renders tabular data with virtualization along the vertical and horizontal axes. It only renders the Grid
cells needed to fill itself based on current horizontal/vertical scroll positions.
If we wanted to render the same list as earlier with a grid layout, assuming our input is a multidimensional array, we could accomplish this using FixedSizeGrid
as
follows:
import
React
from
'react'
;
import
ReactDOM
from
'react-dom'
;
import
{
FixedSizeGrid
as
Grid
}
from
'react-window'
;
const
itemsArray
=
[
[{},{},{},...],
[{},{},{},...],
[{},{},{},...],
[{},{},{},...],
];
const
Cell
=
({
columnIndex
,
rowIndex
,
style
})
=>
(
<
div
className
=
{
columnIndex
%
2
?
rowIndex
%
2
===
0
?
'GridItemOdd'
:
'GridItemEven'
:
rowIndex
%
2
?
'GridItemOdd'
:
'GridItemEven'
}
style
=
{
style
}
>
{
itemsArray
[
rowIndex
][
columnIndex
].
name
}
<
/div>
);
const
Example
=
()
=>
(
<
Grid
className
=
"Grid"
columnCount
=
{
5
}
columnWidth
=
{
100
}
height
=
{
150
}
rowCount
=
{
5
}
rowHeight
=
{
35
}
width
=
{
300
}
>
{
Cell
}
<
/Grid>
);
ReactDOM
.
render
(
<
Example
/>
,
document
.
getElementById
(
'root'
));
Some modern browsers now support CSS content-visibility
. content-visibility:auto
allows you to skip rendering and painting off-screen content until needed. Consider trying the property out if you have a lengthy HTML document with costly rendering.
For rendering lists of dynamic content, I still recommend using a library like
react-window
. It would be hard to have a content-visbility:hidden
version of such a library that beats a version aggressively using display:none
or removing DOM nodes when off-screen like many list virtualization libraries may do today.
Again, use preload sparingly and always measure its impact on production. If the preload for your image is earlier in the document than it is, this can help browsers discover it (and order relative to other resources). When misused, preloading can cause your image to delay FCP (e.g., CSS, fonts)—the opposite of what you want. Also note that for such reprioritization efforts to be effective, it also depends on servers prioritizing requests correctly.
You may also find <link rel="preload">
helpful for cases where you need to fetch scripts without executing them.
A variety of the following web.dev articles touch on how to use preload to:
In this chapter, we discussed some essential considerations that drive the architecture and design of modern web applications. We also saw the different ways in which React.js design patterns address these concerns.
Earlier in this chapter, we introduced the concepts of CSR, SSR, and hydration. JavaScript can have a significant impact on page performance. The choice of rendering techniques can affect how and when the JavaScript is loaded or executed in the page lifecycle. A discussion on Rendering patterns is thus significant when discussing JavaScript patterns and the topic of our next chapter.