Avoid Async Effects In Svelte
Published Nov 20, 2024
Table of Contents
- Side Effects In Svelte
- Avoid Async Effects
- Asynchronously Read Values Are Ignored
- Awaiting Promises Inside Effects
Side Effects In Svelte
If you want to do a side effect in Svelte like fetching data or logging a reactive value to the console when it updates, you can use the $effect
rune:
<script lang="ts">
let count = $state(0)
$effect(() => {
console.log(count)
})
</script>
If you return a function from $effect
, it will be called before the effect re-runs and before it’s destroyed:
<script lang="ts">
let count = $state(0)
$effect(() => {
console.log(count)
return = () => console.log('cleanup')
})
</script>
Effects run on the microtask queue when everything else is done.
In the past you would use the legacy onMount
function to run some code when the component mounts:
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
// do something when the component is ready
$effect(() => {
console.log('mounted')
return () => console.log('cleanup')
})
// this works the same
onMount(() => {
console.log('mounted')
return () => console.log('cleanup')
})
// there's also `onDestroy`
onDestroy(() => console.log('cleanup'))
</script>
The examples look the same but they’re not, and you have to be careful with dependencies inside $effect
and untrack values you don’t want to track.
Avoid Async Effects
Let’s say you have some asynchronous code, so it would also make sense to mark the function that you pass to $effect
as async
:
<script lang="ts">
let count = $state(0)
$effect(async () => {
console.log(count)
return () => console.log('cleanup')
})
</script>
The cleanup function never runs because an async function returns a promise that Svelte doesn’t expect and can’t resolve for you:
let banana = async () => '🍌'
console.log(banana()) // Promise {<fulfilled>: '🍌'}
I assume this is probably because having async effects would cause race conditions, so you would need async versions of these runes like $asyncEffect
.
Asynchronously Read Values Are Ignored
What’s also interesting is that if we read the Svelte docs on understanding dependencies, it says:
“Values that are read asynchronously — after an
await
or inside asetTimeout
, for example — will not be tracked.
The values after await
or inside setTimeout
and then
are not going to be tracked by Svelte:
<script lang="ts">
let count = $state(0)
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
$effect(async () => {
await sleep(1000)
console.log(count) // untracked
})
$effect(() => {
sleep(1000).then(() => {
console.log(count) // untracked
})
})
$effect(() => {
setTimeout(() => {
console.log(count) // untracked
})
})
</script>
If you want to track those values, you could use a poor man’s dependency array inside $effect
:
$effect(async () => {
// has to be before `await`
count
})
$effect(() => {
// has to be outside `setTimeout` or `then`
count
})
Awaiting Promises Inside Effects
Instead of passing an async function to $effect
, you can invoke an async function inside an effect or invoke then
on the promise:
<script lang="ts">
let count = $state(0)
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
// a) using an async function
$effect(() => {
// this can be from anywhere
async function logCount() {
await sleep(1000)
console.log(count)
}
// invoke the async function
logCount()
})
// b) using `then`
$effect(() => {
sleep(1000).then(() => {
console.log(count)
})
})
// c) using an immediately invoked function (IIFE)
$effect(() => {
(async () {
await sleep(1000)
console.log(count)
})()
})
</script>
That’s it! 👍️
Keep in mind, these examples aren’t meant to be reactive, but only show how to use async code inside an effect.