Nelson Michael is a frontend developer and technical writer from Nigeria who enjoys building stuff and sharing what he knows through writing.
Published on
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.
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:
Users would prefer to see something like this instead:
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:
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:
Now let’s move on to building our Card component and CardSkeleton component.
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:
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:
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:
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
Yarn
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:
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:
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:
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:
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:
Finally let’s import our Card and CardSkeleton component into our App component, so that we can we see what we have been building:
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:
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:
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.
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.
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.
"Separation of Concern" is the major reason we might want to use Suspense for data-fetching. Consider the following example:
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:
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:
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.
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:
Here's what our promiseWrapper.js look like:
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:
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.
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:
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:
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
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.
Get visual proof, steps to reproduce and technical logs with one click
Continue reading
Try Bird on your next bug - you’ll love it
“Game changer”
Julie, Head of QA
Try Bird later, from your desktop