Loading States
At some point, your users are going to be waiting for data to load from the server. This could be because they don’t live in a place with state-of-the-art internet service or maybe they just stepped into a tunnel. Regardless, you are going to want to show them something while your request loads. These loading states are sometimes referred to as “Skeleton UIs” and usually display placeholder elements while the actual content is being loaded.
This guide will go over all of the tools that Houdini provides to help you build these interfaces.
A Concrete Example
Before we get too far, let’s look at a concrete example so we can have a goal in mind. For this guide, we’re going to be building a loading screen for the Pokédex that we constructed in the Getting Started guide:
For the sake of this guide, you can imagine that the Pokedex is built using the following route:
query SpeciesInfo($id: Int = 1) {
species(id: $id) {
name
description
evolutionChain {
name
...Sprite_species
}
...Sprite_species
...MoveList_species
...NavButtons_species
}
}
<script lang="ts">
import type { PageData } from './$houdini'
import {
Container,
Panel,
Sprite,
Display,
MoveList,
NavButtons_species,
Number,
} from '~/components'
export let data: PageData
let { SpeciesInfo } = data
$: ({ SpeciesInfo } = data)
$: species = $SpeciesInfo.data?.species
</script>
<Container>
<Panel slot="left">
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite style="height: 96px;" species={evolvedForm} />
<Display>
{evolvedForm.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
</Panel>
</Container>
<script>
import {
Container,
Panel,
Sprite,
Display,
MoveList,
NavButtons_species,
Number,
} from '~/components'
/* @type { import('./$houdini').PageData } */
export let data
let { SpeciesInfo } = data
$: ({ SpeciesInfo } = data)
$: species = $SpeciesInfo.data?.species
</script>
<Container>
<Panel slot="left">
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite style="height: 96px;" species={evolvedForm} />
<Display>
{evolvedForm.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
</Panel>
</Container>
Just in case it’s not clear: the evolutionChain
list corresponds to the 3 columns
in the top right of the loading screen above. If it helps, you can look at a deployed
version of the application here.
The Simplest Solution
If you followed along in the Getting Started Guide then you know that if we click on the
next
button in that example then our application will crash. This is
because data
is null
when the query is loading a new value which causes
$SpeciesInfo.data.species
to explode. The easiest way to protect against
this is to check the fetching
value of the store and render your loading state.
Sorry in advanced about the length of the following example but it highlights one of the primary points I’m trying to illustrate: this approach duplicates a lot of logic and structure.
<script lang="ts">
// ...
$: species = $SpeciesInfo.data?.species
</script>
{#if $SpeciesInfo.fetching}
<Container>
<Panel slot="left">
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each Array.from({length: 3}) as _, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite height={96} loading />
<Display height={30} loading />
</div>
{/each}
</div>
<div class="row">
<Display height={400} loading />
<UpDownButtons disabled />
</div>
<div class="row">
<Button disabled>Previous</Button>
<Button disabled>Next</Button>
</div>
</Panel>
</Container>
{:else}
<Container>
<Panel slot="left">
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={number + 1} />
<Sprite style="height: 96px;" species={node} />
<Display>
{node.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
</Panel>
</Container>
{/if}
<script>
// ...
$: species = $SpeciesInfo.data?.species
</script>
{#if $SpeciesInfo.fetching}
<Container>
<Panel slot="left">
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each Array.from({length: 3}) as _, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite height={96} loading />
<Display height={30} loading />
</div>
{/each}
</div>
<div class="row">
<Display height={400} loading />
<UpDownButtons disabled />
</div>
<div class="row">
<Button disabled>Previous</Button>
<Button disabled>Next</Button>
</div>
</Panel>
</Container>
{:else}
<Container>
<Panel slot="left">
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={number + 1} />
<Sprite style="height: 96px;" species={node} />
<Display>
{node.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
</Panel>
</Container>
{/if}
There are 2 major problems with this:
- We basically have to build our layout twice. Even if we relied heavily on the
?
operator or sprinkled a bunch of#if $SpeciesInfo.fetching
everywhere, we still must duplicate the logic for the evolution chain since whendata
is null, there is no natural list to iterate over to render the boxes. - We had to break the abstraction created by the
MoveList
andNavButtons
components since we needed to duplicate their structure here (something this component is supposed to know nothing about).
Bottom Line? No matter how we approach it, if our goal is to build a loading state that reflects our final UI, relying
on the fetching
value is pretty annoying.
But don’t worry - Houdini solves both of these problems with a single directive: @loading
.
Defining the Shape
Let’s look at how we can address the first point by using the @loading
directive. Simply put, the @loading
directive
is used to describe the desired shape of your loading state. While a network request is pending, the data
value will contain every
field with @loading
starting from the top. Let’s start off simple and see what happens if we just put @loading
at the top of
our query like this:
query SpeciesInfo($id: Int = 1) {
species(id: $id) @loading {
name
description
evolutionChain {
name
...Sprite_species
}
...Sprite_species
...MoveList_species
...NavButtons_species
}
}
If you log the result while fetching, then you’ll see that data
looks something like:
import { PendingValue } from '$houdini'
{
species: PendingValue
}
First, notice that species
isn’t an object with fields in it. That’s because we only put @loading
on the species
field (more on this later). Also, see the value? It might seem a little strange at first, but rather
than using undefined
or something else, you are given a symbol that you can easily use for comparisons
inside of your component:
<script lang="ts">
import { PendingValue } from '$houdini'
// ...
$: species = $SpeciesInfo.data.species
</script>
<Container>
<Panel slot="left">
{#if species === PendingValue}
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
{:else}
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
{/if}
</Panel>
<Panel slot="right">
{#if species === PendingValue}
<div class="evolution-chain">
{#each Array.from({length: 3}) as _, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite height={96} loading />
<Display height={30} loading />
</div>
{/each}
</div>
<div class="row">
<Display height={400} loading />
<UpDownButtons disabled />
</div>
<div class="row">
<Button disabled>Previous</Button>
<Button disabled>Next</Button>
</div>
{:else}
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite style="height: 96px;" species={node} />
<Display>
{node.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
{/if}
</Panel>
</Container>
<script>
import { PendingValue } from '$houdini'
// ...
$: species = $SpeciesInfo.data.species
</script>
<Container>
<Panel slot="left">
{#if species === PendingValue}
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
{:else}
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
{/if}
</Panel>
<Panel slot="right">
{#if species === PendingValue}
<div class="evolution-chain">
{#each Array.from({length: 3}) as _, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite height={96} loading />
<Display height={30} loading />
</div>
{/each}
</div>
<div class="row">
<Display height={400} loading />
<UpDownButtons disabled />
</div>
<div class="row">
<Button disabled>Previous</Button>
<Button disabled>Next</Button>
</div>
{:else}
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
<Sprite style="height: 96px;" species={node} />
<Display>
{node.name}
</Display>
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
{/if}
</Panel>
</Container>
Since our document contains @loading
, data
is no longer null
when fetching.
It’s a small win but it means we can remove the ?
in the definition for species
. It also means
we don’t have to check for fetching
anymore and can start to unify our loading and final UIs
by sprinkling in if
s that check for values in our query results.
That being said, if your first thought was “that doesn’t seem much different” then you are correct. Having
just a single @loading
directive at the top of your document is pretty much the same as what we had before. The real
power starts to show when you use it multiple times in the same document. You see,
Houdini will walk down your query and build your loading state as long as it encounters the directive. The deepest fields
that are tagged with @loading
will be set to the sentinel PendingValue
.
Let’s see this in action:
query SpeciesInfo($id: Int = 1) {
species(id: $id) @loading {
name @loading
description
evolutionChain @loading(count: 3) {
name
...Sprite_species
}
...Sprite_species
...MoveList_species
...NavButtons_species
}
}
With this change, data.species
is always an object. We now have to look at one of its fields to
know if we are loading:
<script lang="ts">
import { PendingValue } from '$houdini'
// ...
$: species = $SpeciesInfo.data.species
</script>
<Container>
<Panel slot="left">
{#if species.name === PendingValue}
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
{:else}
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
{/if}
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
{#if node !== PendingValue}
<Sprite style="height: 96px;" species={node} />
<Display>{node.name}</Display>
{:else}
<Sprite height={96} loading />
<Display height={30} loading />
{/if}
</div>
{/each}
</div>
{#if species.name === PendingValue}
<div class="row">
<Display height={400} loading />
<UpDownButtons disabled />
</div>
<div class="row">
<Button disabled>Previous</Button>
<Button disabled>Next</Button>
</div>
{:else}
<MoveList {species} />
<NavButtons {species} />
{/if}
</Panel>
</Container>
Things are slightly different now that we have multiple applications of @loading
.
data.species
is always safe to use which means our loading value is starting to reflect the final query value
and we can loosen our guards.
However, that’s a tiny win compared to what we were able to do with the evolution chain. Here it is again for quick reference:
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
{#if evolvedForm !== PendingValue}
<Sprite style="height: 96px;" species={node} />
<Display>{node.name}</Display>
{:else}
<Sprite height={96} loading />
<Display height={30} loading />
{/if}
</div>
{/each}
</div>
See that species.evolutionChain
is always safe to iterate over?? This let us pull out all of the repeated
structure and only differentiate the 2 cases where it actually matters. Pretty cool huh?
Houdini makes sure that there is always something we can use to render
and because of how we used @loading
we can compare our values to PendingValue
where its most convenient.
If you’re still not quite sure how this works please check out the DeepDive above. If it’s still confusing, join us
on discord and open a question - we’d love to help. It’s also worth mentioning that 3
is the default value for count
so it wasn’t actually necessary here.
Problem #1 solved. You can use @loading
to create the most convenient shape for your exact needs. When it falls on
lists, Houdini will even create lists with the appropriate values so you always have something to iterate over. This leaves
us with one more thing to clean up: our route is still duplicating the structure of its sub-components.
Composing Loading States
So with the first hurdle addressed, the only thing left is to wire up the fragments so that they can handle their own loading needs. In doing so, our route can stay totally decoupled from the internal structure of the component.
As I’m sure you guessed, this is done with @loading
. All we have to do is mark the fragment spread with @loading
and Houdini
takes care of the rest:
query SpeciesInfo($id: Int = 1) {
species(id: $id) @loading {
name @loading
description
evolutionChain @loading(count: 3) {
name
...Sprite_species
}
...Sprite_species
...MoveList_species @loading
...NavButtons_species @loading
}
}
<Container>
<Panel slot="left">
{#if species.name === PendingValue}
<Display height={30} loading />
<Sprite style="flex-grow:1" loading />
<Display height={120} loading/>
{:else}
<Display>
{species.name}
</Display>
<Sprite {species} />
<Display>
{species.description}
</Display>
{/if}
</Panel>
<Panel slot="right">
<div class="evolution-chain">
{#each species.evolutionChain as evolvedForm, i }
<div class="evolution-form">
<Number value={i + 1} />
{#if node !== PendingValue}
<Sprite style="height: 96px;" species={node} />
<Display>{node.name}</Display>
{:else}
<Sprite height={96} loading />
<Display height={30} loading />
{/if}
</div>
{/each}
</div>
<MoveList {species} />
<NavButtons {species} />
</Panel>
</Container>
Pretty straightforward, right? So what did this take? Well, not much. Here’s a rough example of what NavButtons
looks like:
<script lang="ts">
import type { NavButtons_species } from '$houdini'
export let species: NavButtons_species
$: data = fragment(species, graphql(`
fragment NavButtons_species on Species {
id @loading
}
`))
$: pending = $data.id === PendingValue
</script>
<div class="row">
<Button disabled={pending || $data.id <= 1}>
Previous
</Button>
<Button disabled={pending || $data.id >= 151}>
Next
</Button>
</div>
<script>
/* @type { import('$houdini').NavButtons_species } */
export let species
$: data = fragment(species, graphql(`
fragment NavButtons_species on Species {
id @loading
}
`))
$: pending = $data.id === PendingValue
</script>
<div class="row">
<Button disabled={pending || $data.id <= 1}>
Previous
</Button>
<Button disabled={pending || $data.id >= 151}>
Next
</Button>
</div>
Just like with the query, the only thing we had to do is use @loading
on a value that we could
compare against to render our loading state.
Final Thoughts
Thanks for making it all the way through this guide! I hope you found it useful and that it illustrated the power of treating your loading states as a first class concern in your fragments. These examples could be taken further by breaking the route into separate components with fragments that own each section of the UI and its loading state and I encourage you to do so in your own application. I think you’ll find that being able to reuse these bits of UI will make your designers happier and your application will look better with less effort.