Svelte has a reactivity system powered by something called runes. You use a rune to declare a state, this is similar to the useState hook from React. Runes are a part of the language itself.
ex.
<script>
let count = $state(0);
// this is a rune!
function increment(){
count += 1
}
</script>
<button onclick={increment}>
Clicked {count} {count == 1 ? 'time' : 'times'}!
</button>Deep state
The state reacts to reassignments, and it also reacts to mutations. The latter is called deep reactivity. This means that if your state is an array for example, it changes with each mutation or push. You can mutate the array in Svelte; this was not the case in React.
Deep reactivity is implemented using JavaScript proxies.
You can use deep reactivity in cases where you have a global/universal state that’s imported from an external module (like globals.svelte.js). This is because you can’t directly reassign an import, but you can mutate it; for ex. importedState += 1 is invalid while importedState.counter += 1 is totally valid. You could also not directly export the state, as suggested by the docs, and instead export a getter/setter pair of functions.
Deriving state
Often times you would need to derive one state from another; we use the $derivedrune for this. The expression inside the $derived declaration will be reevaluated whenever its dependencies (in the below case, numbers) are updated.
<script>
let numbers = $state([1, 2, 3, 4]);
let total = $derived(numbers.reduce((total, number) => total+number, 0))
</script>Inspecting state
You cannot use standard console.log() to inspect state, because it’s a reactive proxy. Instead, you can use console.log($state.snapshot(numbers)) (to inspect the numbers state from above) or better yet you can use a $inspect rune which automatically logs to the console whenever there’s a state change. You can use it as: $inspect(numbers).
Shared state or Universal Reactivity
You can use runes outside components to share some global state, ex.
export const counter = $state({
count = 0
});And then import this file later in some other .svelte to make use of the shared state.
Effects
A state is only reactive if something is reacting to it. The thing that reacts to a state is called an effect. You can create an effect using the $effect rune.
It’s to be noted that an effect is just an escape hatch, rather than something to use frequently. If you can put yout side effects in an event handler for example, that’s preferable.
A standard effect is a function that runs when the state updates, and can be used for things like calling third party libraries, making network requests, etc. These only run in the browser and not during server side rendering. You should NOT update the state inside effects as it will lead to never-ending update cycles.
Effects run after a component has been mounted to the DOM, in a microtask after state changes. Re-runs are batched and happen after any DOM updates have been applied. An effect can return a teardown function which will run immediately before the effect re-runs. These functions also run for example when a component is unmounted or when a parent effect re-runs.
ex.
<script lang="ts">
let count = $state(0);
let milliseconds = $state(1000);
$effect(() => {
// This will be recreated whenever `milliseconds` changes
const interval = setInterval(() => {
count += 1;
}, milliseconds);
return () => {
// if a teardown function is provided, it will run
// a) immediately before the effect re-runs
// b) when the component is destroyed
clearInterval(interval);
};
});
</script>
<h1>{count}</h1>
<button onclick={() => (milliseconds *= 2)}>slower</button>
<button onclick={() => (milliseconds /= 2)}>faster</button>$effect automatically picks up any reactive changes (state, derived, props) that are synchronously read inside its function body, and registers them as its dependencies. This also includes changes that are more indirect, i.e. inside function calls. When these dependencies change, the $effect schedules a re-run.
This can cause infinite loops.
Think about this for a second - let’s say that the state you’re reading iscounter. Inside the effect, you docounter = counter + 1. Now you have assigned a new value to the state after reading it (we readcounterand assignedcounter + 1tocounter), and because the state was registered as a dependency (as it was read and written synchronously), the effect schedules a re-run.
In this re-run, you again setcounter += 1and the effect schedules yet another re-run. This goes on ad infinitum. This is the precise reason why you should never synchronously update a state inside an$effect.
It’s interesting that the Svelte compiler warns you before this blows up your browser tabs. In my case, it correctly gave me a helpful message that read that the value I was updating probably shouldn’t be state.
As a general rule of thumb, NEVER read and write to the same state synchronously within an$effect. If you’re running into this infinite loop, you’ve probably committed this mistake.
See the section below on when not to use an $effect.
It is to be noted that values that are read asynchronously, like after an await or a setTimeout, will NOT be tracked (or reigstered as dependencies), and will NOT schedule a re-run. So if you’re updating to a state after an await or after a .then(), that’s a valid pattern and would not lead to the problem described above. Thismeans that you can synchronise state asynchronously inside an $effect and it will not be tracked.
Thus something like this..
<script lang="ts">
let finalData: object = $state({})
$effect( () => {
async function badFetch(url: string) {
const response: Response = await fetch(url);
if (response.ok) {
const jsonResponse = await response.json(); // this resolves a promise at a later point in time
finalData = jsonResponse; // note that finalData is $state. This just assigns the value of a resolved JSON to our state. We are updating the state here.
console.log(finalData); // we're trying to read the state here.
// does not cause an infinite loop even though we're both reading and updating the state here, the value is NOT a dependency as it's being read and updated asynchronously.
}
}
badFetch(API_URL);
});
</script>..is a valid (but ineffective) pattern in a non SvelteKit project. (I am only talking about the “fetching data from an API” part here - not the idea of synchronising state asynchronously.) The reason I called the function badFetch and the pattern ineffective is because firstly, the effect is triggered after the DOM mounts, so there will be a tiny delay between you seeing empty components and fetched data. Yes, you could handle this using an {#if} block and use an {#else} to render a loading component, but more importantly, $effect is an escape hatch and should NOT be used to synchronise state (what we’re doing here), synchronously or asynchronously. This is just not the idiomatic way to fetch data. Yes, it can be done, but there are better ways to do this exact thing. See the pattern on Fetching for more details, and how the task of fetching data from an API can be better accomplished (in more idiomatic ways) with Svelte-native markup rather than using an $effect quirk.
But anyways, it’s useful to keep in mind that untracked read/writes (as inside an async function) are invisible to an $effect’s dependency system, as dependency tracking only happens during an effect’s synchronous execution window.
This is a decent mental model for $effects that Claude gave me:
$effect is Svelte’s boundary between its own reactive world and everything outside of it. Everything inside Svelte’s reactive system, such as $state, $derived, is still just Svelte managing the data that your template reads. $effect is what you reach for the moment you need to do something to the world outside the system, the DOM1 like document or window, a network request, a third-party library, localStorage, API request, etc. Nothing outside of Svelte’s reactivity knows or cares about $state on its own - $effect is a deliberate bridge that lets a change in your reactive state cause something to happen out there. Think of an $effect as an escape-hatch for synchronising with systems outside of Svelte. Using it merely to trigger a side-effect like fetching data from an API is anti-pattern and not the idiomatic way, as discussed previously.
Think of it as a standing function, not a one time command. “Run this block now, then watch whatever reactive values you touched while running it, and if any of them change later, run it again”. For why cleanup functions matter, read about one of the problems I faced in an earlier React project.
Effects can come in two flavors:
- reactive: the block reads state synchronously, and should re-run every time that state changes. Ex. a canvas element as given in the Svelte docs.
- one-time setup/teardown: the block reads nothing reactive, so it runs once on mount, sits idle and only its cleanup ever fires again on unmount. Ex. a CTRL+K listener.
If the thing you’re building produces a value other parts of your UI need to read, use $derived. If it produces no value, only a side effect on the outside world (and maybe a way to undo that effect), or just takes an action,that’s $effect.
When NOT to use effects
In general, $effect is best considered something of an escape hatch - useful for things like analytics and direct DOM manipulation - rather than a tool you should use frequently. It’s us stepping outside Svelte’s reactivity system in order to synchronise something with the “outside world”.
Avoid using it to synchronise state. Use $derived for that. For ex. instead of this:
<script>
let count = $state(0);
let doubled = $state();
// don't do this!
$effect(() => {
doubled = count * 2;
});
</script>do this..
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>For things that are more complicated than a simple expression like count+2, you can also use $derived.by.
If you’re using an effect because you want to be able to reassign the derived value, note that deriveds can be directly overridden. Refer to the docs.
You might be tempted to do something convoluted with effects to link value to one another.
The following example shows two inputs for “money spent” and “money left” that are connected to each other. If you update one, the other should update accordingly.
But instead of this..
<script lang="ts">
const total = 100;
let spent = $state(0);
let left = $state(total);
$effect(() => {
left = total - spent;
});
$effect(() => {
spent = total - left;
});
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={left} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>…you should use oninput callbacks or better still, function bindings
<script lang="ts">
const total = 100;
let spent = $state(0);
let left = $derived(total - spent);
function updateLeft(left) {
spent = total - left;
}
</script>
<label>
<input type="range" bind:value={spent} max={total} />
{spent}/{total} spent
</label>
<label>
<input type="range" bind:value={() => left, updateLeft} max={total} />
{left}/{total} left
</label>
<style>
label {
display: flex;
gap: 0.5em;
}
</style>If you absolutely have to (synchronously or asynchronously) update $state within an effect, and run into infinte loop because you read and write to the same $state, use untrack. See the docs.
Footnotes
-
Note that the DOM is the “outside world” too - Svelte actually uses effects under the hood to update the template’s DOM as well. When you write
<h1>{message}</h1>, Svelte’s compiler creates a hidden$effectthat updates that specifich1’s text node whenmessagechanges. ↩