Svelte in Depth The $effect.tracking rune
Svelte 5 is finally here! While many things have become simpler, some new
features may be a bit trickier to understand. In this series, we’ll dive into
the more advanced aspects in detail. This first post focuses on the
$effect.tracking
rune, exploring what it does and when you might want to use
it.
Definition
The Svelte team has already enhanced the documentation quite a bit, so there’s not much to add. Here’s the official description:
The
$effect.tracking
rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template.This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
They then provide an example of how a rune with subscriptions can be implemented. It’s concise, but understanding why and when you might need it may not be immediately obvious.
Why is it Needed?
To understand why this rune was added, it’s helpful to know which functionality it replaces.
In Svelte 4, there were two reactivity models:
- Simply declaring variables in a Svelte component, letting the compiler handle reactivity.
- Stores that could be passed around (these still exist in Svelte 5).
Readable stores have a straightforward contract: you can subscribe to them to receive updates. To subscribe to a store, you would write:
const unsubscribe = myStore.subscribe((value) => console.log(value))
// And when you’re done:
unsubscribe()
Thanks to this subscriber pattern, it’s easy to run code both when the store is subscribed to and when all subscribers unsubscribe.
With the built-in readable
and writable
store functions, it’s as simple as
this:
import { readable } from 'svelte/store'
const initialValue = 'hello'
const myStore = readable(initialValue, () => {
console.log('The subscriber count went from 0 to 1')
return () => {
console.log('All subscribers have unsubscribed')
}
})
Even without the readable
helper function from Svelte it would be quite easy
to implement this functionality.
Naive Implementation
Let’s say we wanted to implement similar subscriber count functionality in
Svelte 5, with runes instead of stores. To do this, we can use the
$effect()
rune which creates a tracking context managed by the component’s
lifecycle.
Here’s a naive (but incorrect) approach to implementing our own simplified
readable
function with signals:
// $lib/utils.svelte.js
// ❌ Don't do this! This is an example of how not to implement this.
export const readable = (initial_value, start) => {
let value = $state(initial_value)
let subscribers = 0
let stop = null
return {
// We return an object with a getter so we don't lose reactivity
get value() {
$effect(() => {
// ✨ Here is the trick ✨
// Every time the value is accessed, we create an effect in
// which we increase the subscriber count. An $effect() will
// run automatically when the scope is created (i.e.: the
// component is mounted)...
if (subscribers === 0) {
// If there are no subscribers yet, invoke the start function
// and store the returned function as the `stop` function.
stop = start()
}
// Increase our subscriber count.
subscribers++
// ...and will automatically be cleaned up when the scope is
// destroyed (i.e.: the component is unmounted). Here we can
// decrease the subscriber count.
return () => {
subscribers--
if (subscribers === 0) {
// If it was the last subscriber, invoke the stop function
stop?.()
stop = null
}
}
})
// Return our reactive value.
return value
}
}
}
This function is nearly identical to the built-in svelte/store
readable
implementation and would be used like this:
<script>
import { readable } from '$lib/utils.svelte.js'
const shoppingCartItems = readable(0, () => {
console.log('We have a subscriber!')
return () => {
console.log('We have no subscribers anymore')
}
})
</script>
Items in cart: {shoppingCartItems.value}
If you try this, you’ll see that it works! When this component mounts, the
start
function will be invoked, and the value will be reactive. So, why do we
need $effect.tracking
?
The Problem
With Svelte 5’s new reactivity via signals, we can now use reactivity outside of components. Svelte proxies the underlying value and handles the logic to update our code when necessary, so we no longer need to subscribe to a store and can simply access the value directly.
However, here’s the issue: when accessing the underlying value outside of a
tracking context (i.e., outside an $effect
), how does the readable
implementation know that accessing the property shouldn’t increase the
subscriber count?
Here’s an example:
<script>
// Our readable implementation again
import { readable } from '$lib/utils.svelte.js'
const shoppingCartItems = readable(0, () => {
console.log('Start')
return () => console.log('Stop')
})
// Accessing the reactive value
console.log(shoppingCartItems.value)
</script>
The reactive value is read when the component is initialized and our naive
readable
implementation will increase the subscriber count to 1
. But the
value is accessed outside of a tracking context. Simply accessing a value in
the initializer does not track the value — the code will not rerun if the
value changes. If we wanted to track it, we would need to put the access in an
$effect()
. This means that our readable implementation thinks that there is a
subscriber although there is none.
Imagine that we use this readable
in our app, and start polling for updated
values as soon as there is a subscriber. With this implementation, our code will
always think there is a subscriber and start polling in the background,
although there actually are none — defeating the purpose of tracking subscribers
in the first place.
Solution
To solve this issue, we can use the $effect.tracking
rune. This rune tells us
whether we’re currently inside a tracking context, which is exactly what we
need. We only want to increase our subscriber count if the value has been
accessed in a tracking context (i.e.: an $effect()
) because only then will
the code accessing the value rerun when the value changes.
So, we just need to add this check in our value
getter:
// $lib/utils.svelte.js
export const readable = (initial_value, start) => {
let value = $state(initial_value)
let subscribers = 0
let stop = null
return {
// We return an object with a getter so we don't lose reactivity
get value() {
if ($effect.tracking()) {
$effect(() => {
// Unchanged
})
}
return value
}
}
}
The full implementation can be found in the
$effect.tracking
docs.
Additional Uses
As the name suggests, the primary purpose of the $effect.tracking()
rune is to
check whether your code is running in a tracking context. However, the rune also
serves an additional purpose.
Effects are only permitted to run within a scope. If you attempt to run an
effect outside a component, you will encounter the following error:
effect_orphan $effect can only be used inside an effect (e.g. during component initialisation)
.
In our readable
implementation, we want code outside a scope to still be able
to access the value. By wrapping our use of $effect()
inside an
$effect.tracking()
check, we effectively "feed two birds with one scone":
- We ensure that the subscriber count doesn't increase if accessed outside of a tracking context, and
- We avoid encountering an error if the value is accessed outside of a tracking scope.
I hope this was helpful and answered any questions you might have had about the
$effect.tracking
rune. Thanks for reading.
Need a Break?
I built Pausly to help people like us step away from the screen for just a few minutes and move in ways that refresh both body and mind. Whether you’re coding, designing, or writing, a quick break can make all the difference.
Give it a try