Contents

    Guides

    Implementing Skeleton Screen In React With React Loading Skeleton and Suspense

    Nelson Michael

    Nelson Michael is a frontend developer and technical writer from Nigeria who enjoys building stuff and sharing what he knows through writing.

    Published on

    September 14, 2022
    Implementing Skeleton Screen In React With React Loading Skeleton and Suspense

    Skeleton screens are quite popular for websites. Using them improves user experience in several ways;  including providing visual feedback when an operation, such as data fetching, is taking place.

    When performing these kinds of render blocking operations we need a way to provide a fall back UI so that users don't just see a blank screen. React lets us do this effortlessly with the new Suspense component.

    In this article, you'll learn about Suspense, a new way of handling asynchronous operations in React. It will also demonstrate how to implement Skeleton screens in React with React Loading Skeleton and use it along with Suspense when fetching data in order to improve UX.

    Pre-requisites

    • Have node installed on your computer
    • Set up a react app with codesandbox or with create-react-app or from scratch
    • Basic JavaScript and React knowledge

    What is React Loading Skeleton?

    A skeleton screen is an UI that looks similar to the original structure of a page when all of its content is loaded. React loading skeleton is a JavaScript library that allows you to create these beautiful, animated loading skeletons, and with react-loading-skeleton, the skeletons adapt to your app automatically.

    Skeleton screens that use animations, such as a shimmer effect, tend to make the page's load time appear shorter. Thankfully, we don't have to worry about animations because react-loading-skeleton comes with a shimmer animation out of the box.

    Let's face it nobody wants to look at a blank screen like this:

    Skeleton screen error view

    Users would prefer to see something like this instead:

    The correct view of the skeleton screen

    Building the UI

    Let's begin by creating the UI for our project, which will include a Card component and a CardSkeleton component that will serve as the suspense component's fall-back UI when performing a network request.

    To start, we'll create a new react project. You can start by choosing the most convenient method for creating a react project for you, such as create-react-app or starting from scratch. But for the purposes of this tutorial, I'll be utilizing codesandbox.io.

    Note: We will create three codesandbox projects: one for building and configuring the skeleton UI, another for integrating Suspense into our project without any abstractions, and the last for using suspense with a data-fetching library that support suspense.

    Let's get this party started now;

    Start with creating two directories in the src folder of our project: assets and components, as well as a main.scss file for styling. We'll leave these two folders empty for the time being and return to them when we're ready.

    Now let's build our App component in the App.js file:

    // JavaScript [React]
    import "./main.scss";
    export default function App() {
      return (
        <div className="App">
          <div className="card-container">
          </div>
        </div>
      );
    }

    It's a bit sparse, but it'll suffice for the time being. At the top of the file we import our stylesheet main.scss; this is where we will style our entire project. This will do since it is not a large-scale application.

    Next, let's give our project some initial styling in the main.scss file:

    // CSS [Sass]
    @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;400;800&display=swap');
    body{
      padding: 1em;
      margin: 0;
      background-color: #ebebeb;
      font-family: "Poppins", sans-serif;
      box-sizing: border-box;
      min-height: 100vh;
    }

    Now let’s move on to building our Card component and CardSkeleton component.

    Building the Card UI

    Let's build our card UI, which will be displayed to users when an API call is made and data is provided to the client.

    If you've been following along nicely, this is how our folder structure should look:

    Look of folder structure

    It's nothing fancy, but it does the job. If you're working on a major project, I'd recommend that you follow a correct architectural pattern.

    Now, in our component folder, create a Card.js file; this is where we will build our Card component.

    Here’s our card component:

    // JavaScript [React]
    import dp from "../assets/dp.jpg";
    const Card = () => {
      return (
        <div className="card">
          <div className="img-holder">
            <img src={dp} alt="user avater" />
          </div>
          <div className="desc">
            <span>
              <h3>Nelson Michael</h3>
              <p>The Pastry maker</p>
            </span>
            <p className="bio">
              Money trees is the perfect place for shade and that's just how i feel.
              Nevertheless, the best things come from living outside of your comfort
              zone.
            </p>
          </div>
        </div>
      );
    };
    export default Card;

    At the top of the file we import a dummy image from our assets folder that would serve as a display image in our cards, then we complete the mark-up for our card UI.

    Now let’s add a bit of styling to our card component:

    // CSS [Sass]
      .card{
        padding:.35rem;
        border:1px solid #c6c6c6;
        border-radius:8px;
        display: flex;
        gap:10px;
        .img-holder{
          min-width:60px;
          height:60px;
          img{
            border-radius: 50%;
            width:100%;
            height:100%;
            object-fit:cover;
            object-position: 50% 50%;
          }
        }
        .desc{
          span{
            h3,p{
              margin:0;
            }
            h3{
              font-size:14px;
              color:#010101;
            }
            p{
              font-size:11px;
              color:#BDBDBD;
            }
          }
          .bio{
            font-size: 12px;
          }
        }
      }

    Building the Skeleton UI

    In our codesandbox under the dependency panel, search for “react-loading-skeleton” and install it as a dependency in your project.

    If you setup your project with create-react-app or from scratch, simply add react-loading-skeleton into your project like so:

    npm

    npm install react-loading-skeleton

    Yarn

    yarn add react-loading-skeleton

    Now we can start using react-loading-skeleton in our project, by simply importing and using it wherever we want. Let’s do that :

    In our component folder, create a new file called CardSkeleton.js. This would hold our skeleton UI.

    Here’s our CardSkeleton component:

    import Skeleton from "react-loading-skeleton";
    import "react-loading-skeleton/dist/skeleton.css";
    const CardSkeleton = ({ amount }) => {
      const loadCards = Array(amount).fill(1);
      return loadCards.map((_, i) => (
        <div className="card-skeleton" key={i}>
          <div>
            <Skeleton circle width={60} height={60} />
          </div>
          <div>
            <Skeleton count={5} />
          </div>
        </div>
      ));
    };
    export default CardSkeleton;

    Let’s go over this component so that we know how to use react-loading-skeleton:

    To use react-loading-skeleton, we need three things:

    1. The Skeleton component
    2. skeleton.css file imported on line 2
    3. SkeletonTheme component (optional)

    You can style individual Skeleton components by using props for each one, or you can use the SkeletonTheme component to style all Skeleton components below it in the react hierarchy.

    It's similar to setting up a provider component, thus you should ideally do it at the top-level component of your react application.

    In our project, I'm doing this in the index.js file where I'm importing our App component:

    // JavaScript [React]
    
    import { StrictMode } from "react";
    import { createRoot } from "react-dom/client";
    import App from "./App";
    import { SkeletonTheme } from "react-loading-skeleton";
    const rootElement = document.getElementById("root");
    const root = createRoot(rootElement);
    root.render(
      <StrictMode>
        <SkeletonTheme baseColor="#d9d9d9">
          <App />
        </SkeletonTheme>
      </StrictMode>
    );

    In the code above I’m using the SkeletonTheme component to set a base color for all the Skeleton components in our application.

    The amazing thing about react-loading-skeleton is that, it adapts to the styles you already have defined. Here’s an example of what that means:

    <h1>{props.title || }</h1>

    The code above means that if the title value is available it would display that, otherwise it would display a Skeleton loader that adjusts to the already defined styles of the h1 element.

    In our project however, we’re creating a separate component(CardSkeleton) for our skeleton UI.

    Our CardSkeleton component takes in a single prop called amount, which is passed into the Array(amount).fill(1) to simulate an array with a length of whatever value is passed as the amount.

    Next we use that array to create as many CardSkeleton components as we want, all we have to do is just pass in the amount we want into the amount prop.  

    Now, notice that the Skeleton component itself takes in some props? react-loading-skeleton provides a number of props, that we can use to customize the component to our liking.

    The count prop specifies the number of skeleton props we want and the circle prop makes a circular Skeleton component by setting the border-radius to 50% and of course the width and height props.

    Here’s a full reference for all the props available for react-loading-skeleton.

    Now let’s add some styling to the CardSkeleton component in our main.scss file so that it looks like our Card component:

    // CSS [Sass]
    
    .card-skeleton{
      padding:.35rem;
      border:1px solid #c6c6c6;
      border-radius:8px;
     display:flex;
     gap:10px;
     div:last-child{
       flex-grow: 1;
     }
     div:first-child{
       height:60px;
       width:60px;
     }
    }

    Finally let’s import our Card and CardSkeleton component into our App component, so that we can we see what we have been building:

    // JavaScript [React]
    
    import "./main.scss";
    import Card from "./components/Card";
    import CardSkeleton from "./components/CardSkeleton";
    export default function App() {
      return (
        <div className="App">
          <div className="card-container">
            <Card />
            <Card />
            <CardSkeleton amount={10} />
          </div>
        </div>
      );
    }

    Notice that our App component has a div element with a className = "card-container", so in our main.scss let’s style that class like so:

    // CSS [Sass]
    
    body{
      ...
    }
    .card-container{
      max-width:600px;
      margin:0 auto;
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 0.5rem;
      .card{
        ...
      }
      .card-skeleton{
          ...
      }
    }

    We use the .card-container class to wrap our .card and .card-skeleton classes

    If you’ve been following along correctly, you should be seeing this:

    The correct view of the Skeleton UI once properly implemented

    Here’s our first codesandbox for this project:

    https://codesandbox.io/s/setting-up-skeleton-ui-bb0gcx?file=/src/components/CardSkeleton.js

    Great, now we have our UI all setup, let’s move on to how to use suspense and then complete what we have been building so far.

    What is React Suspense?

    The new Suspense component in React allows you to handle asynchronous operations. It's a simple and effective approach to letting React know when your component is awaiting data.

    Suspense allows you to render a fall-back component declaratively while a component that should be rendered is awaiting the completion of some asynchronous activity, for example, a network request. that Suspense is not a data-fetching or state management library.

    Let's look at a graphical comparison of how we would normally handle asynchronous actions against how we would handle the same operation using React Suspense.

    View of graphical components using React Suspence.

    In the conventional way of dealing with asynchronous operations, a variable called isLoading is used to track the status of the request. If true, we display a Loader component to inform the user of the current state.

    Notice that there is no state variable in the Suspense method; instead, the loading state is managed by React using Suspense, which then allows us to render a fall-back component declaratively.

    In the previous example, because React had no knowledge of the network request, we had to control the loading state using the isLoading variable. With Suspense, React is aware that a network call is in progress, and by wrapping the Card component with Suspense, it delays the render until the network call is completed.

    You might be wondering how React knows about a loaded state. Currently, React 18 provides Suspense, however Suspense for data-fetching is handled by data-fetching libraries, and there are two popular alternatives (Relay and SWR) that have Suspense integrations.

    With the help of these data-fetching libraries, react can detect a loading state and handle it seamlessly.

    Why do we need Suspense?

    "Separation of Concern" is the major reason we might want to use Suspense for data-fetching. Consider the following example:

    // JavaScript [React]
    
    const OldMethod=({userId})=>{
      const [data, isLoading] = useGetData(userId);
      
      if(isLoading){
        return <Loader />
      }
    
    return <Card name={data.fullname} />
    }

    In the preceding example, we're mixing concerns: fetching the data and displaying the loading state. We don't have to worry about this with Suspense, because it handles the loading state for us and shows a fall-back UI if we're in a loading state.

    Another reason Suspense is useful is that it synchronizes various components to appear once they have finished fetching data.

    This is what I mean:

    Diagram of component organization using Suspense

    In the preceding example, our components Card and 'List' are both attempting to obtain data. Because this process is asynchronous, they may receive their data at separate times, resulting in the Card component displaying first or vice versa. This may not be a good user experience.

    One solution is to move the data-fetching logic from both components to their parent component App and handle the data-fetching in there, ensuring that Card and List only display when the data from the App component is ready, or even better, we could simply handle this with Suspense, as shown below:

    // JavaScript [React]
    
    <Suspense fallback={<Loader />}>
      <Card />
      <List />
    </Suspense>

    Suspense makes it simple for us to deal with the previously discussed issue.

    The last step in the tutorial is to put everything together. So, in the following section, we'll look at how we can use react-loading-skeleton and Suspense to fetch data.

    Putting it All Together (Integrating skeleton UI with suspense for data-fetching)

    In this tutorial, we'll be using the SWR data-fetching library to make network calls; here, we can simply enable the suspense option to notify Suspense of our component's current load state.

    Before we use the simple way let me show you how to write a similar abstraction to what these data-fetching libraries provide to us so that we have an idea how it works under the hood.

    Create a new folder called api in our src folder, and in this folder, create two new files called promiseWrapper.js and getData.js. If you got that correctly our folder structure should look like this:

    Screenshot view of folder structure implemented correctlty

    Here's what our promiseWrapper.js look like:

    // JavaScript
    
    const promiseWrapper = (promise) => {
      let status = "pending";
      let response;
      const suspend = promise.then(
        (res) => {
          status = "success";
          response = res;
        },
        (err) => {
          status = "error";
          response = err;
        }
      );
      return {
        read() {
          if (status === "pending") {
            throw suspend;
          } else if (status === "error") {
            throw response;
          } else {
            return response;
          }
        }
      };
    };
    export default promiseWrapper;

    Our promiseWrapper function wraps a Promise and provides a method for determining whether the data returned by the Promise is ready to be read. If the Promise succeeds, it returns the resolved data; if it fails, it throws an error; and if it is still waiting, it returns the Promise.

    We define two variables within our promiseWrapper function:

    1. A status variable that tracks the promise argument's current state is provided.
    2. A response variable that stores the promise's rejection or resolution.

    The status variable is set to "pending" by default because it is the default state of any new Promise. Next, we create a new variable, suspend, assign it to the Promise, and chain a then method to it.

    We have two callback methods inside the then method: one for when the value is resolved and one for when it rejects. If the Promise is successfully resolved, the status variable is set to "success," and the response variable is assigned to the resolved result. However, if the Promise fails, we set the 'status' variable to "error" and the response variable to the rejected value.

    Next, we define a new function called read, and within it, we include an if logic that examines the value of the status variable. If the promise's status is "pending," we throw the suspender variable we just defined. We throw the response variable if it is an "error." Finally, if it is anything other than the two for example "success", we return the response variable.

    We throw either the suspender variable or the error response variable to signal to Suspense that the Promise has not yet been resolved.

    And we're doing it by simulating an error in the component by using throw, which the Suspense component will intercept. The Suspense component then examines the thrown data to decide whether it is an error or a Promise.

    Finally, we return our object containing the 'read()' method; this is how our React components will interact with the Promise and receive its value.

    Now, let's look at the getData.js file; here's how the code looks:

    //JavaScript
    
    import promiseWrapper from "./promiseWrapper";
    const getData = (url) => {
      const promise = fetch(url)
        .then((res) => res.json())
        .then((res) => res.data);
      return promiseWrapper(promise);
    };
    export default getData;

    At the top of the file, we simply import our promiseWrapper function. Because our function is expected to take in a promise, we take in a url and perform a Fetch request with the url as its argument. The result, which is a promise, is passed into the variable promise that we declared.

    Finally, we pass the variable promise to the promiseWrapper function. Now we can use our getData method to make API requests, and React will be notified and Suspense will work properly.

    Let’s put the final pieces together; we'll use the getData function in our Card.js file to make our api call.

    // JavaScript [React]
    
    import dp from "../assets/dp.jpg";
    import getData from "../api/getData";
    const resource = getData(
      "https://run.mocky.io/v3/7f89db3f-1f93-444c-8c42-b84f653a073e"
    );
    const Card = () => {
      const data = resource.read();
      return data.map((user, i) => (
        <div className="card" key={i}>
          <div className="img-holder">
            <img src={dp} alt="user avater" />
          </div>
          <div className="desc">
            <span>
              <h3>{user.name}</h3>
              <p>The Pastry maker</p>
            </span>
            <p className="bio">
              Money trees is the perfect place for shade and that's just how i feel.
              Nevertheless, the best things come from living outside of your comfort
              zone.
            </p>
          </div>
        </div>
      ));
    };
    export default Card;

    Line 2 includes the import of our getData method. Following that, we pass an api endpoint to that function and save the result in a resource variable.

    This resource variable is an object that has a reference to the request Promise, which we may access by invoking the .read() method. If the request has not yet been handled, calling resource.read() will return an exception to the Suspense component.

    If it is, it will return the resolved data of the Promise, which in this case would be an array of user data. This array is then mapped over to render each user card.

    The final step is to use Suspense in our App component to handle our asynchronous operation. Let's look at how:

    // JavaScript [React]
    
    import { Suspense } from "react";
    import "./main.scss";
    import Card from "./components/Card";
    import CardSkeleton from "./components/CardSkeleton";
    export default function App() {
      return (
        <div className="App">
          <div className="card-container">
            <Suspense fallback={<CardSkeleton amount={10} />}>
              <Card />
            </Suspense>
          </div>
        </div>
      );
    }

    Now that React is aware of a network request and its loading state, we can simply wrap our Card component in Suspense and supply a fall-back component (CardSkeleton) so that whenever our card component tries to get some data, our fall-back component is rendered in its place.

    Here's our second codesandbox:
    https://codesandbox.io/s/example-without-data-fetching-library-0vqunn?file=/src/App.js

    Now that we’ve seen how to use Suspense without any abstraction, let's see how we can quickly and easily do it with a data-fetching (SWR) library that has support for Suspense.

    The only change we have to make is in our Card component:

    // JavaScript [React]
    
    import useSWR from 'swr'
    import dp from "../assets/dp.jpg";
    const fetcher = (...args) => fetch(...args).then(res => res.json())
    const Card = () => {
      const { data } = useSWR('https://run.mocky.io/v3/7f89db3f-1f93-444c-8c42-b84f653a073e', fetcher, { suspense: true })
      
      return data.data.map((user, i) => (
        <div className="card" key={i}>
          <div className="img-holder">
            <img src={dp} alt="user avatar" />
          </div>
          <div className="desc">
            <span>
              <h3>{user.name}</h3>
              <p>The Pastry maker</p>
            </span>
            <p className="bio">
              Money trees is the perfect place for shade and that's just how i feel.
              Nevertheless, the best things come from living outside of your comfort
              zone.
            </p>
          </div>
        </div>
      ));
    };
    export default Card;

    First we install swr as a dependency in our project, here's an article on how to get started with swr  then all we have to do is enable suspense in the useSWR hook and we're good to go.

    Here's our third and final codesandbox:

    https://codesandbox.io/s/example-with-data-fetching-library-ey3yf0?file=/src/components/Card.js

    Conclusion

    Skeleton screens provide a great experience for when there is data fetching. The idea being to hold some loading indicator that mimics your UI, it get's old when you're waiting forever so this approach provides a neat way for users to see that something is actually happening and with Suspense in React we are able to achieve this by providing our skeleton UI as the fall back component when data fetching is taking place.

    Data-rich bug reports loved by everyone

    Get visual proof, steps to reproduce and technical logs with one click

    Make bug reporting 50% faster and 100% less painful

    Rating LogosStars
    4.6
    |
    Category leader

    Liked the article? Spread the word

    Put your knowledge to practice

    Try Bird on your next bug - you’ll love it

    “Game changer”

    Julie, Head of QA

    star-ratingstar-ratingstar-ratingstar-ratingstar-rating

    Overall rating: 4.7/5

    Try Bird later, from your desktop