Table of Contents
- Using Fetch to Consume APIs with Svelte
- Getting references to Components generated in an #each block
- Passing attributes to component DOM element
- Client-Side Storage with Svelte
Working with external data in Svelte is important. Here's a guide.
Maintainers of this Recipe: swyx
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:
- https://svelte.dev/docs#2_Assignments_are_reactive
- https://svelte.dev/docs#onMount
- https://svelte.dev/docs#Attributes_and_props
- https://svelte.dev/docs#await
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.
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:
- In local development (making requests from
http://localhost
tohttps://myapi.com
) - In production (making requests from
https://mydomain.com
tohttps://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:
- https://alligator.io/nodejs/solve-cors-once-and-for-all-netlify-dev/
- https://zeit.co/docs/v2/serverless-functions/introduction
- https://docs.begin.com/en/http-functions/api-reference
- https://aws.amazon.com/blogs/mobile/amplify-framework-local-mocking/
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.
- Svelte Suspense discussion: sveltejs/svelte#1736
- Your link here?
Using bind:this
allows a component to store a reference to it's children, this can also be used when generating a series of components in an {#each}
block.
Method 1: Using an array
This method simply binds the generated component to an array element based on it's index within the loop.
<script>
import Child from "./Child.svelte";
const array = [
{ id: 1, title: "apple" },
{ id: 2, title: "banana" },
];
const children = [];
</script>
{#each array as item, i}
<Child title="{item.title}" bind:this="{children[i]}" />
{/each}
Method 2: Using an object as a hashtable
An alternative is to use an unique key and bind the component to an object, effectively making a hashtable of components.
<script>
import Child from "./Child.svelte";
const array = [
{ id: 1, title: "apple" },
{ id: 2, title: "banana" },
];
const children = [];
</script>
{#each array as item, i (id)}
<Child title="{item.title}" bind:this="{children[id]}" />
{/each}
When you want to "forward" any attributes like class
or style
to your component wrapper DOM element (instead of declaring variables), use $$restProps
:
<!-- Component.svelte -->
<li {...$$restProps} ><slot></slot></li>
<!-- App.svelte -->
<Component class="li-item-class">{name}</Component>
Svelte Playground here. See relevant part of docs for more.
This is helpful where, for example, using MDSveX, you want to create a bunch of Svelte wrappers to DOM elements like H1
, P
, and A
instead of the normal h1
, p
, and a
.
Note that when passing a class to component, you may need to set it to global :global(.title){...}
Persistant state using client-side storage enables users to access data even when they are offline. These recipes are ways to persist state in Svelte using Local Storage.
Local Storage uses a key/value system for storing data. It is limited to storing only simple values but complex data can be stored if you encode and decode the values with JSON. In general, Local Storage is appropriate for smaller sets of data you would want to persist - things like user preferences or form data. Larger data with more complex storage needs would be better suited for other means.
<script>
let value = localStorage.getItem("name") || "";
function saveToLocal() {
localStorage.setItem("name", value);
}
</script>
<div>
<h1>Svelte with Basic Local Storage</h1>
<input type="text" bind:value />
<button on:click="{saveToLocal}">Save to Local</button>
<p>{value}</p>
</div>
Type something in the input
field and hit the button
. When you refresh the page, voila the data has persisted and is used as the default value for the value
prop.
If you want to automatically store it without a save, use a $:
<script>
let lStore = {};
let storeKey = "MY_STORE";
try {
lstore = JSON.parse(localStorage.getItem(storeKey));
} catch (e) {}
$: if (lStore) {
localStorage.setItem(storeKey, JSON.stringify(lStore));
}
lStore.name = lStore.name || 'world';
</script>
<input bind:value="{lStore.name}" />
<h1>Hello {lStore.name}!</h1>
Since Local Storage only works with simple values, more complex values like objects or arrays must be serialized and deserialized with JSON in order to store them in Local Storage.
<script>
import { onMount } from "svelte";
let todos = [];
let newTodo;
function addTodo() {
// ensure they actually typed something
if (!newTodo) {
return;
}
todos = [...todos, newTodo];
newTodo = "";
saveTodosToLocal();
}
function removeTodo(index) {
const length = todos.length;
todos = [...todos.slice(0, index), ...todos.slice(index + 1, length)];
console.log(todos);
saveTodosToLocal();
}
function saveTodosToLocal() {
const parsed = JSON.stringify(todos);
localStorage.setItem("todos", parsed);
}
onMount(() => {
if (localStorage.getItem("todos")) {
try {
todos = JSON.parse(localStorage.getItem("todos"));
} catch (e) {
localStorage.removeItem("todos");
}
}
});
</script>
<div>
<h2>Todo List</h2>
<ul>
{#each todos as todo, i}
<li>
<span>{todo}</span>
<button on:click={() => removeTodo(i)}>Remove Todo</button>
</li>
{/each}
</ul>
<p>
<input type="text" bind:value={newTodo} />
<button on:click={addTodo}>Add Todo</button>
</p>
</div>
Here the data is initialized as an empty array and then onMount
is used to get any prior data stored in Local Storage. Since the data has to be deserialized first this approach is used instead of the previous version where the data was initialized with any existing Local Storage result.
The rest is pretty standard Svelte Todo app business.
For cases when you need more than localized component state, Svelte offers stores. Svelte stores, however, do not persist data. We're going wrap the svelte writable
store implementation with some Local Storage functionality.
// localStorageStore.js
import { writable, get } from "svelte/store";
export function localStorageWritable(key, initialValue) {
// retrieve existing Local Storage value if it exists
if (localStorage.getItem(key)) {
initialValue = JSON.parse(localStorage.getItem(key));
}
// store the initial state in Local Storage
localStorage.setItem(key, JSON.stringify(initialValue));
// create the store
const store = writable(initialValue);
// return svelte writable interface
return {
set(newValue) {
localStorage.setItem(key, JSON.stringify(newValue));
store.set(newValue);
},
update(cb) {
const currentState = get(store);
this.set(cb(currentState));
},
subscribe: store.subscribe,
};
}