Chapter 12. React.js Design Patterns

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.

An Introduction to React

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.

Terminology Used

We will use the following terms frequently in this chapter. Let’s quickly look at what each of them means:

React/React.js/ReactJS

React library, created by Facebook in 2013

ReactDOM

The react-dom package providing DOM-specific methods for client and server rendering

JSX

Syntax extension to JavaScript

Redux

Centralized state container

Hooks

A new way to use state and other React features without writing a class

ReactNative

The library to develop cross-platform native apps with JavaScript

webpack

JavaScript module bundler, popular in React community

Single-page application (SPA)

A web app that loads new content on the same page without a full page refresh/reload.

Basic Concepts

Before we discuss React design patterns, it would be helpful to understand some basic concepts used in React:

JSX

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

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

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

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.

Client-side rendering

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.

Server-side rendering

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.

Hydration

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.

Creating a new app

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:

Higher-Order Components

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.

Composing

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.

Pros

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.

Cons

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.

Render Props Pattern

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} />}

Lifting State

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>
  );
}

Children as a Function

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.

Pros

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.

Cons

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.

Hooks Pattern

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.

Class Components

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.

Restructuring

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.

Complexity

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.

Hooks

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.

State Hook

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..."
      />
    );
  }

Effect Hook

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.

Custom Hooks

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:

Additional Hooks Guidance

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.

Pros and Cons of Using Hooks

Here are some benefits of making use of Hooks:

Fewer lines of code

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>
  );
}
Simplifies complex components

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.

Reusing stateful logic

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.

Sharing nonvisual logic

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).

React Hooks Versus Classes

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.

Static Import

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.

Dynamic Import

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.

Loadable Components

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;

Import on Interaction

In the chat application example, we dynamically imported the EmojiPicker component when the user clicked on the Emoji. This type of dynamic import is called Import on Interaction. We triggered the component import on interaction by the user.

Import on Visibility

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;

Code-Splitting

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.

Route-based Splitting

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.

Bundle Splitting

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.

PRPL Pattern

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.

Loading Prioritization

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.

Preload in Single-Page Apps

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.

Preload + the async Hack

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>

Preload in Chrome 95+

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

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.

How Does Windowing/Virtualization Work?

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

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

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'));

Improvements in the Web Platform

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.

Conclusions

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:

Summary

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.