DEV Community

Brian K
Brian K

Posted on

Suspense in Svelte: Writing Components That Don't Care

Loading data, managing async requests, and communicating status information back to the end user takes an impressive portion of our time writing web applications. What would it look like if we could write components that did not care about any part of that?

Our Component

Let's write a simple component that displays all the possible evolutions of a Pokemon using the data supplied from pokeapi.com.

<script>
// file: Evolution.svelte
import Link from './link.svelte'
import { getEvolution, getPokemon } from '$lib/data.js'

export let id
$: pokemon = getPokemon(id)
$: evolution = getEvolution($pokemon?.evolution_chain, $pokemon?.id)
</script>

{#if $evolution?.length}
  {#each $evolution as id}
    <Link { id } />
  {/each}
{:else}
  <p>This Pokemon doesn't evolve!</p>
{/if}
Enter fullscreen mode Exit fullscreen mode

We've accomplished our goal! This component is really simple and straightforward but also does not care about loading states, error handling, or data fetching. Unfortunately, something has to worry about those aspects in a production application. What can we do to allow us to write components this simple without compromising?

Data Fetching

As part of the "do not care" mantra, we want to avoid knowing if any other component needs the same data. Let's just make the request and let our data layer worry about caching and pooling requests between various components.

An implementation of our getPokemon function might look like this:

// File: $lib/data.js
import { swr } from '@svelte-drama/swr'
import { suspend } from '@svelte-drama/swr/plugin'

export function getPokemon (id: number) {
  const url = `https://pokeapi.co/api/v2/pokemon-species/${ id }/`
  const { data } = swr(url, {
    plugins: [suspend()]
  })
  return data
}
Enter fullscreen mode Exit fullscreen mode

@svelte-drama/swr will cache every request keyed on the url passed to it. If multiple components request the same key at the same time, only one request will be made and all the components will be updated when it returns. If this request has been made before, we can even skip making the request at all and just return the cached data.

Using the suspend plugin here notifies our application that we need certain data and this component isn't ready to render until we have finished fetching that data. Exactly what that means is in the next section.

Finally, data returned here is a Svelte store. It will start off as undefined while fetching data, which our component unfortunately does need to be aware of, and will update to our data once the request is finished.

Suspense

To fit the final piece of the puzzle, we still need to show loading indicators to the user. Let's take our <Evolution> component and wrap it in a page that looks like this:

<script>
// index.svelte
import { Suspense } from '@svelte-drama/suspense'
import Evolution from './Evolution.svelte'
</script>

<h1>Eevee Evolutions</h1>
<Suspense>
  <Evolution id={ 133 } />
  <p slot="loading">Loading...</p>
  <p slot="error">An error occurred.</p>
</Suspense>
Enter fullscreen mode Exit fullscreen mode

The <Suspense> component here is tied into the suspend call we made while fetching data. If any child components (or any of their children) aren't ready to display yet, this component will just show "Loading...". Once the data comes in, the loading indicator is discard and our components are shown.

Isn't this just #await?

{#await} in templates is a powerful tool and these tools do not replace it. If all data fetching and loading indicators happen in a single component, then that is a simpler way to achieve these same goals.

The difference happens when data loading is spread across multiple components. A change to the APIs <Evolution> depends on would only impact that single component. The loading indicators on our main page do not care which data is needed or where it comes from. If the <Link> component referenced in our <Evolution> component also fetched data of its own (e.g. prefetching an image of the Pokemon) we don't have to change any code here at all.

Conclusion

This isn't meant to be an in depth explanation for using any of the libraries mentioned here. For that, consult their documentation:

Instead, hopefully it illuminates their motivations for existing and what problems they are trying to solve. You can find a full fledged example of the techniques discussed here: https://pokemon-suspense-demo.vercel.app/

Discussion (0)