Using Fetch to Consume APIs with Svelte

Working with external data in Svelte is important. Here’s a guide.

Maintainers of this Recipe: swyx

Fetching on Component Mount in Svelte

Method 1: Using Lifecycles

We can declare a data variable and use the onMount lifecycle to fetch on mount and display data in our component:

<!-- https://svelte.dev/repl/99c18a89f05d4682baa83cb673135f05?version=3.20.1 -->
<script>
  import { onMount } from "svelte";
  let data;
  onMount(async () => {
    data = await fetch(
      "https://api.coindesk.com/v1/bpi/currentprice.json"
    ).then((x) => x.json());
  });
</script>

<pre>
  {JSON.stringify(data, null, 2)}
</pre>

You can further improve this implementation by showing a placeholder while data is undefined and also showing an error notification if an error occurs.

Method 2: Using Await Blocks

Since it is very common to update your app based on the status of your data fetching, Svelte offers convenient await blocks to help.

This example is exactly equal to Method 1 above:

<!-- https://svelte.dev/repl/977486a651a34eb5bd9167f989ae3e71?version=3.20.1 -->
<script>
  let promise = fetch(
    "https://api.coindesk.com/v1/bpi/currentprice.json"
  ).then((x) => x.json());
</script>

{#await promise}
<!-- optionally show something while promise is pending -->
{:then data}
<!-- promise was fulfilled -->
<pre>
		{JSON.stringify(data, null, 2)}
	</pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}

Here you can see that it is very intuitive where to place your loading placeholder and error display.

Related Reading:

Fetching on Button Click in Svelte

One flaw with the above approach is that it does not offer a way for the user to refetch data, and additionally we may not want to render on mount (for data saving or UX reasons).

Method 1: Simple Click Handler

If we don’t want to immediately load data on component mount, we can wait for user interaction instead:

<!-- https://svelte.dev/repl/2a8db7627c4744008203ecf12806eb1f?version=3.20.1 -->
<script>
  let data;
  const handleClick = async () => {
    data = await fetch(
      "https://api.coindesk.com/v1/bpi/currentprice.json"
    ).then((x) => x.json());
  };
</script>

<button on:click="{handleClick}">
  Click to Load Data
</button>
<pre>
  {JSON.stringify(data, null, 2)}
</pre>

The user now has an intuitive way to refresh their data.

However, there are some problems with this approach. You may still need to declare an extra variable to display error state. More subtly, when the user clicks for a refresh, the stale data still displays on screen, if you are not careful.

Method 2: Await Blocks

It would be better to make all these commonplace UI idioms declarative. Await blocks to the rescue again:

<!-- https://svelte.dev/repl/98ec1a9a45af4d75ac5bbcb1b5bcb160?version=3.20.1 -->
<script>
  let promise;
  const handleClick = () => {
    promise = fetch(
      "https://api.coindesk.com/v1/bpi/currentprice.json"
    ).then((x) => x.json());
  };
</script>

<button on:click="{handleClick}">
  Click to Load Data
</button>

{#await promise}
<!-- optionally show something while promise is pending -->
{:then data}
<!-- promise was fulfilled -->
<pre>
    {JSON.stringify(data, null, 2)}
  </pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}

The trick here is we can simply reassign the promise to trigger a refetch, which then also clears the UI of stale data while fetching.

Method 3: Promise Swapping

Of course, it is up to you what UX you want - you may wish to keep displaying stale data and merely display a loading indicator instead while fetching the new data. Here’s a possible solution using a second promise to execute the data fetching while the main promise stays onscreen:

<!-- https://svelte.dev/repl/21e932515ab24a6fb7ab6d411cce2799?version=3.20.1 -->
<script>
  let promise1, promise2;
  const handleClick = () => {
    promise2 = new Promise((res) =>
      setTimeout(() => res(Math.random()), 1000)
    ).then((x) => {
      promise1 = promise2;
      return x;
    });
  };
</script>

<button on:click="{handleClick}">
  Click to Load Data {#await promise2}🌀{/await}
</button>

{#await promise1}
<!-- optionally show something while promise is pending -->
{:then value}
<!-- promise was fulfilled -->
<pre>
    {value}
  </pre
>
{:catch error}
<!-- optionally show something while promise was rejected -->
{/await}

Method 4: Data Stores

One small flaw with our examples so far is that Svelte will still try to update components that unmount while a promise is still inflight. This is a memory leak that sometimes causes bugs and ugly errors in the console. We should ideally try to cancel our promise if it is unmounted, but of course promise cancellation isn’t common. When a component unmounts, Svelte cancels its reactive subscriptions, but an unmounted component has some other issues that Svelte doesn’t clean up:

  • it can still dispatch events
  • it can still call callbacks
  • other code queued to run after a promise fulfills will still run, possibly causing unwanted side effects

It can be simpler to keep promises out of components, and only put async logic in Svelte Stores, where you read values and trigger custom methods to update values.

// https://svelte.dev/repl/483ce4b0743f41238584076baadb9fe7?version=3.20.1
// store.js
import { writable } from "svelte/store";

export const count = writable(0);
export const isFetching = writable(false);

export function getNewCount() {
  isFetching.set(true);
  return new Promise((res) =>
    setTimeout(() => {
      res(count.set(Math.random()));
      isFetching.set(false);
    }, 1000)
  );
}
<script>
  import { getNewCount, count, isFetching } from "./store";
</script>

<button on:click="{getNewCount}">
  Click to Load Data {#if $isFetching}🌀{/if}
</button>

<pre>
{$count}
</pre>

This has the added benefit of keeping state around if the component gets remounted again with no need for a new data fetch.

Dealing with CORS Errors in Svelte

Svelte is purely a frontend framework, so it will be subject to the same CORS restrictions that any frontend framework faces. You will run into CORS issues in two ways:

  1. In local development (making requests from http://localhost to https://myapi.com)
  2. In production (making requests from https://mydomain.com to https://theirapi.com)

You can solve both with a range of solutions from having a local API dev server or proxying requests through a serverless function or API gateway. None are responsibilities of Svelte but here are some helpful resources that may help:

If you happen to be running a Sapper app, then you may take advantage of preloading data server-side in Sapper: https://sapper.svelte.dev/docs#Preloading.

Further Links