DEV Community

Cover image for Make Your Website's User Experience Better with React Suspense
Nathanael
Nathanael

Posted on

Make Your Website's User Experience Better with React Suspense

You've probably been in a situation where you visited a website and that site took a bit of time to load, you most likely had to sit down waiting as the loading animation repeated itself. Slow loading speeds are often due to the large amount of code or data the site is loading at once. Having to wait for the site to load could be extremely annoying, not to mention a waste of time.

Trying to navigate through the website as the website continued to load was probably not the best experience, and you probably had to wait until the browser had loaded all the code necessary for the site to function.

If you're a React developer, you can massively speed up site load time by indicating an order of render for components using lazy() and <Suspense>.

React lazy()

lazy() is a function that relies on dynamic import and reduces your website or web app load time by code splitting.

Lazy-loading a component tells the browser that the component isn't a priority, the browser will load all other components and load a lazy-loaded component only when a user navigates to that component or route.

Lazy loading offers several advantages, some of which are:

  • Cut down resources needed to load a site
  • Reduce the time it takes a user to interact with a site
  • Improve a website's user experience

Is Lazy loading necessary?

We'll answer this using an example. The code snippet below is from a React app with a Navbar component and two other components named PageOne and PageTwo

import PageOne from "./components/PageOne";
import PageTwo from "./components/PageTwo";
import { Routes, Route } from "react-router-dom";
import Navbar from "./components/Navbar";
function App() {
  return (
    <>
      <Navbar />
      <Routes>
        <Route path="/" element={<PageOne />} />
        <Route
          path="pagetwo" element={<PageTwo />}
        />
      </Routes>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The PageOne and PageTwo components contain two images each.

On initial load, we land on our index or home page shown in the image below

Home page of a website showing two adorable cats

I have not navigated to the Second Page/PageTwo component.

However, the browser has already loaded all the components in the background. To confirm this, I'll navigate to the Sources tab in the Dev tools of chrome browser(If you use Firefox this tab is called 'debugging')

A screenshot of chrome's devtools with a red arrow pointing to the components file tree

There's an arrow pointing to the components folder in the file tree of the React App on the left side of the image. You can see the browser has already loaded the PageTwo component even though I haven't navigated there yet. This could be an unnecessary drain of user resources, you might wonder, how?

Let's assume you're a content creator that has certain content available for paid users only, loading that content in the background for free users even though they can't access it would not make much sense.

Lazy-loading that premium content will fix this and prevent the browser from loading that content unless a user can access it.

Getting Started with lazy()

Lazy-loading a component takes a different approach from the traditional import() method. I've modified the App component to demonstrate lazy loading

import { lazy, Suspense} from "react";
import { Routes, Route } from "react-router-dom";
import Navbar from "./components/Navbar";
import PageOne from "./components/PageOne";
const PageTwo = lazy(() => {import("./components/PageTwo")});

function App() {  
  return (
    <>
      <Navbar />
      <Routes>
        <Route path="/" element={<PageOne />} />
        <Route
          path="pagetwo"
          element={
            <Suspense fallback="Loading...">
              <PageTwo />
            </Suspense>
          }
        />
      </Routes>
    </>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's go over the modifications made to the App component

To lazy load a component, the first thing you need to do is import lazy and Suspense from React and you can see this below.

import { lazy, Suspense } from 'react'
Enter fullscreen mode Exit fullscreen mode

The next step to take is to import your component in the lazy function block and assign it to a variable. You can see this in the example below. The PageTwo component is now being imported in the lazy function block and declared as a variable.

const PageTwo = lazy(() => {import("./components/PageTwo")});
Enter fullscreen mode Exit fullscreen mode

Note - Lazy imports should be done outside a function component and not inside. This means you have to declare them at the top with any other exports

When you're done importing your lazy component, you need to wrap it in <Suspense>. A lazy-loaded component or route must be wrapped in <Suspense>

<Suspense> displays a fallback whenever a user navigates to a component and the component is being loaded. You'll learn more about <Suspense> and fallback as you read on

The fallback in <Suspense> is used to display any React component or text you want to render while your component loads. A fallback is important as it gives visual feedback that the component is loading.

<Route
          path="pagetwo"
          element={
            <Suspense fallback="Loading...">
              <PageTwo />
            </Suspense>
          }
        />
Enter fullscreen mode Exit fullscreen mode

If you're wrapping a route component in Suspense, it has to be within the curly braces of the Route element.

The route has now been lazy-loaded and after reloading the React App the change is evident in the Dev tools

A screenshot of chrome's devtools but this time a component has been lazy loaded and isn't loading unnecessarily

Check it out, the PageTwo component was not loaded by the browser on initial render. The component will only be loaded when I navigate there. I've included an animated image below to show this in effect.

An animated image showing how a component only loads when I navigate there and not when the website is loaded

The browser only loaded PageTwo when I navigated there and not on the initial render as it did before. This is a huge performance boost as the code and files loaded on the initial render are now smaller than before. Now that you've seen how good lazy-loading is, it's time to see what <Suspense> does.

React Suspense

The initial explanation of <Suspense> was brief and in this section, you'll learn more about <Suspense>. You'll learn:

  • How to use one <Suspense> for multiple components.

  • How to use multiple <Suspense> for different components

  • How to add an ErrorBoundary to <Suspense>

The PageTwo component used in the section discussing lazy loading contains a <Suspense> with a fallback with the text "Loading..." The image below shows what the suspense displayed while the component was loaded in the background

A screenshot showing a site's homepage with a navbar and a loading text on screen

A <Suspense> fallback would normally be a loader component with a spinner or a skeleton loader, but we'll keep things simple in this project.

Something worth mentioning is that you can use <Suspense> on components that aren't lazy loaded.

Wrap multiple elements in one suspense

You can wrap several components in a single <Suspense>. Multiple components wrapped in a <Suspense> will be loaded at the same time with a single fallback.

The code below has three components wrapped in a single <Suspense>.

import LightComponent from "./components/LightComponent";
import { lazy, Suspense } from "react";
const MediumComponent = lazy(() =>(import("./components/MediumComponent")
));
const HeavyComponent = lazy(() => (import("./components/HeavyComponent")
));
function App() {
  return (
    <section>
      <Suspense fallback="Loading">
        <LightComponent />
        <MediumComponent />
        <HeavyComponent />
      </Suspense>
    </section>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

There are three components of varying render times in the code snippet above, they are:

  • LightComponent - Contains a simple text.

  • MediumComponent - Contains an image.

  • HeavyComponent - Contains an image.

All the components are in the same <Suspense> with a fallback. MediumComponent and HeavyComponent are lazy-loaded, You may have noticed that LightComponent isn't lazy-loaded but still works with <Suspense>. As I mentioned, you don't need to lazy load a component for it to work with <Suspense>

Placing all three components in the same <Suspense> means all three components will be completely loaded before rendering. The result is the image below

Image of a black screen with a simple text of "Loading"

One or all of the three components is not ready to render to the screen, this causes the fallback to be displayed until all components are ready. After fully loading all the components, it renders them all at once

A homepage of a site showing two adorable cats

You can see from the above example that wrapping all your elements in one <Suspense> will load all your components at once, even if some of the components are already loaded and ready to render. The page will only display the components when everything is loaded and ready to render.

Nesting suspense

You've seen that wrapping several elements in the same <Suspense> might not always be the best option. You can wrap your components in different <Suspense> if you have components with varying sizes.

Check the code snippet below.

import LightComponent from "./components/LightComponent";
import { lazy, Suspense } from "react";
const MediumComponent = lazy(() =>(import("./components/MediumComponent")
));
const HeavyComponent = lazy(() => (import("./components/HeavyComponent")
));
function App() {
  return (
    <section>
      <Suspense>
        <Suspense fallback="Loading Light Component">
          <LightComponent />
        </Suspense>
        <Suspense fallback="Loading Medium Component">
          <MediumComponent />
        </Suspense>
        <Suspense fallback="Loading Heavy Component">
          <HeavyComponent />
        </Suspense>
      </Suspense>
    </section>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Notice how each component has its own <Suspense>. Wrapping each component with its <Suspense> means it will now be rendered in order of readiness. The order of render for our App would now be:

  1. The entire App Component would start loading with a fallback of Loading...

  2. LightComponent will get rendered to the screen immediately after it finishes loading.

Image of a black screen with simple text showing the sequence of loading of components, one components has rendered while the rest are still loading

  1. The fallbacks for MediumComponent and HeavyComponent will be displayed as those components continue fetching data.

  2. MediumComponent will be rendered once fully loaded while the fallback for HeavyComponent remains as HeavyComponent keeps fetching data.

Image of a black screen with simple text showing the sequence of loading of components, two components have rendered but the last one is still loading

  1. HeavyComponent gets rendered immediately after it fully loads.

All images have been fully rendered to the screen

You've seen how <Suspense> can greatly improve the quality of your websites or web apps. However, just because you can wrap multiple components in <Suspense> does not mean you should. You should wrap only heavy or lazy-loaded components in <Suspense>, Wrapping components with only texts or light images in suspense would not make much sense.

Some things you can wrap in <Suspense> are:

  • A component that makes a call to an API

  • A component with a lot of images

  • Routes

“Simplicity boils down to two steps: Identify the essential. Eliminate the rest.”

Leo Babauta

Improve Suspense with an Error Boundary

You can level up your code even further by adding an error boundary to <Suspense>. An error boundary catches errors in your code that could otherwise break your app.

If a component wrapped in <Suspense> without an error boundary throws an error, your app will only display a blank page which wouldn't be very helpful. <Suspense> with an <ErrorBoundary>, however, will display a fallback instead of a blank page

Add an error boundary

To add <ErrorBoundary> to <Suspense>, you need to install react-error-boundary as a dependency using either npm or yarn install.

Follow these steps to add <ErrorBoundary> to <Suspense>.

  1. Navigate to your terminal and type npm install react-error-boundary if you use the node package manager or yarn add react-error-boundary if you use yarn.

  2. Import <ErrorBoundary> into your app at the top

    import { ErrorBoundary } from "react-error-boundary"
    
  3. Wrap your suspense in <ErrorBoundary> and provide a fallback for <ErrorBoundary>. Using a code snippet from one of our earlier examples to demonstrate this, we have

            <ErrorBoundary fallback={<p>Something went wrong</p>}>
                <Suspense fallback="Loading Heavy Component">
                  <HeavyComponent />
                </Suspense>
            </ErrorBoundary>
    

    The fallback makes sure you or your users can see an error message instead of a blank page, doing this greatly improves the user experience of your website.

You can go one step further to improve ErrorBoundary by adding an ErrorFallback component instead of a simple text, with an ErrorFallback component you can display a proper error message, together with a button to refresh the page or reset your app state. You can create an ErrorFallback by following these steps.

  1. Create a new React component called ErrorFallback and in that component add the following code

    const ErrorFallback = ({ error, resetErrorBoundary }) => {
      return (
        <div>
            <p>Something went wrong</p>
            <pre>{error.message}</pre>
            <button onClick={resetErrorBoundary}>Try again</button>
        </div>
      )
    }
    
    export default ErrorFallback
    

    Error Fallback takes in two props:

    error is the error thrown by the component wrapped in the error boundary.

    resetErrorBoundary is a function that resets your state or App to its original state before an error was thrown

  2. Import the ErrorFallback component in your App component or wherever you used the error boundary

    import ErrorFallback from "./components/ErrorFallback";
    
  3. Replace the fallback prop in the error boundary with a new prop called FallbackComponent

    <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => /*add your code to reset state here*/}> 
        <Suspense fallback="Loading Heavy Component"> 
            <HeavyComponent /> 
        </Suspense> 
    </ErrorBoundary>
    

    Add the ErrorFallback component to the FallbackComponent

  4. Add a function called onReset to ErrorBoundary the onReset function takes in a code used to reset the state of the parent component to its original state before an error was thrown. It's up to you to provide the code

Conclusion

In this article you have seen how to:

  • Lazy-load components.

  • Add <Suspense> to or Suspend your components.

  • Add fallback to <Suspense>.

  • Add ErrorBoundary to <Suspense> to catch any errors.

You've also seen that you need to add <Suspense> to lazy components, but you don't need to lazy load a component before using <Suspense>

Add lazy() and <Suspense> to appropriate sections of your code and you'll improve your website's user experience a lot. If you're interested in learning further, you can check the official react docs.

If you have any comments or questions please feel free to ask them or reach out to me on Twitter where I'm most active

Top comments (0)