This is a walkthrough on how to implement a skeleton loading infrastructure into a front-end application. The approach builds skeleton states as a system so your front-end can scale while maintaining consistency for loading states. I’ll use React, React Context and Hooks for this example.
Implement a “skeleton state” for the lowest level components in your UI. Then use the state management feature of your chosen framework to toggle on and off the skeleton state when necessary.
Since higher level components are composed of low level components, each new component moving forward (or upward) gets a skeleton state view for free. It’s this concept that allows an application to scale up without the technical overhead of building out unique skeleton states for each new component while the application grows.
Low level components
Good low level components to start with are
<Icon/>. These commonly found components behave as expected, i.e., text renders text, image renders an image, etc. But in this approach, they will all be built with their own subsequent “loading” view that can be toggled on and off.
By default, the text component will render whatever text and styles are passed in. If the prop
skeleton is passed in as
true, it will render a bar with a grey background and rounded edges like the image examples from above.
The image component follows the same principle. It will render an image with whatever source is passed in. But if the
skeleton prop is
true, it will render a grey box to denote “loading”.
High level components
As I mentioned before, higher level components are composed out of the low level components and therefore come packaged with a subsequent skeleton view. A
<DetailsCard/>, for example, might look like this.
Notice how code for the skeleton state isn’t required for this component, yet a skeleton view is immediately available? It’s not exactly obvious that a skeleton state is available since there’s no mention of it in the code, but that’s partly the idea. This component is composed of “skeleton-enabled” components, therefore becoming “skeleton-enabled” itself. Any new components that are built as the application evolves get on-demand loading states for free.
Scalability is also notably achieved because managing the look and feel of your application’s skeleton state happens in a completely different area of the app (low level components).To illustrate this point, imagine introducing an animation effect on all the skeleton states across an application. By applying this to a low level component, the animation effect immediately gets applied to every component.
That’s really it for the bones of this approach. The next part goes into how skeleton states are toggled on and off.
Consider an application that has a search field to find github repos. Entering a term in the search field kicks off an API request to github. The response comes back in the form of a list that populates below the search field. While the request is on its way out, and back, the UI reveals a skeleton state to help the user understand what’s going on.
A simple implementation of this in React might have a
<Header/> component for the title area, a
<Search/> component for the search box, and a
<RepoList/> component to render the search result.
Note that the
onSubmit callback on line 25 fires the
handleSearchSubmit function to make the API request. Line 7 sets the local
isLoading state to
true when the request starts, and returns to
false on line 19 when the request completes. On Line18, the response from the API request is stored in the
gitRepos local state. Lastly, the
isLoading states are passed into the
<RepoList/> component, which renders the results from the search query, takes an array of data and renders a list of
<DetailsCard/> components. It also takes an
isLoading prop to understand when data is being fetched. In the example below, it swaps the real data for a list of fake placeholder data (line 11) when
isLoading is set to true.
Passing data downwards
While the API request is in progress, you’ll need to tell the low level components to enter their skeleton states. In React, you can pass props downwards but this would require prop drilling in order to reach the low level components. It’s very cumbersome to keep track of props as more and more components are nested, which is why prop drilling is generally not advised.
Instead, we’ll use React Context. With React Context, you can share state with any child component regardless of how many levels deep.
To do this, we’ll create a new file called
skeleton.js. It has a
SkeletonContext variable to store a boolean value for toggling on and off the skeleton state. And a
<SkeletonArea/> provider component to share the state.
Next we’ll wrap the entire application in
app.js with the new
<SkeletonArea/> component (lines 22 and 28 below) and pass in the
isLoading property. By passing in the
isLoading property to the provider, you’re effectively sharing that value as global state for all of its child components.
Lastly, we’ll update all the low level components with the new context provider so they know when to enter their “skeleton” state. Each file below has a new boolean variable called
isLoading that is set by the context provider.
Isolated skeleton areas
There is an issue with this solution. Since all the components are using the same state you might get a skeleton view for an area of a page that should not be associated with “loading”. The header here, for example, should not enter a skeleton state when the search response is being fetched (“loading”).
To get around this, you can nest React context providers at each point where the boundary of a group of components should share the same skeleton state. In the diagram below, the provider only wraps the list of results which creates an isolated area of global state for that particular tree of components.
Let’s update the code to isolate the list results, like the diagram above. Since we only want the skeleton state to appear on the results list, we’ll remove it from
app.js and add it to the
Now we have the desired behaviour. Only the list area shows a skeleton state.
Everything works now, but the code needs to be packaged up for reuse. For that, I’ll capture the pattern details in a React hook and expose a small API for reuse. I’ll call the hook
useSkeletonData and the API for the hook will take 3 parameters.
fakeData: this is data for components to consume while they’re in a skeleton state. Think of it as placeholder data.
realData: this is data for components to consume while they’re not in a skeleton state.
isLoadingPredicate: this takes a function that returns a boolean. Its purpose is to remotely control when any low level child components should enter their skeleton state.
The hook (line 18) will return a data object, either real or placeholder, depending on the predicate function described above, and a “
connector” object. (The connector is a necessary piece that allows the hook to control the skeleton state of the low level components).
With the new hook available, I’ll apply it to the
Line 16 takes the real data, line 17 takes the fake data, and line 18 takes the predicate function. In this case it’s returning the
isLoading prop to control the skeleton state. Lastly, line 10 has the
connector object passed in the context component.
Back on the
skeleton.js file, I’ll create one more hook for the low level components (line 16 below),
Any low level components moving forward will be able to use this hook instead of importing React Context directly to toggle on and off their skeleton view. Here’s how the
<Text/> component would be updated.
All the other low level components would be updated as well.
We’ve solved a number of complicated problems:
- On demand skeleton components
- Isolated skeleton areas
- Reusable API
This sandbox has a running example.
Thanks for reading ❤
- —— -
Nathaniel is a Solution Architect at Rangle.io