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:

I
II
III

For the sake of this guide, you can imagine that the Pokedex is built using the following route:

src/routes/[[id]]/+page.qgl
query SpeciesInfo($id: Int = 1) {
    species(id: $id) {
        name
        description
        evolutionChain {
            name
            ...Sprite_species
        }

        ...Sprite_species
        ...MoveList_species
        ...NavButtons_species
    }
}
src/routes/[[id]]/+page.svelte
<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>
src/routes/[[id]]/+page.svelte
<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.

src/routes/[[id]]/+page.svelte
<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}
src/routes/[[id]]/+page.svelte
<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:

  1. 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 when data is null, there is no natural list to iterate over to render the boxes.
  2. We had to break the abstraction created by the MoveList and NavButtons 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:

src/routes/[[id]]/+page.svelte
<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>
src/routes/[[id]]/+page.svelte
<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 ifs 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:

src/routes/[[id]]/+page.svelte
<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:

src/routes/[[id]]/+page.gql
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
    }
}
src/routes/[[id]]/+page.svelte
<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:

src/components/NavButtons.svelte
<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>
src/components/NavButtons.svelte
<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.