In Svelte 5, you can create reactive state anywhere in your app — not just at the top level of your components.
Suppose we have a component like this:
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
clicks: {count}
</button>
We can encapsulate this logic in a function, so that it can be used in multiple places:
<script>
function createCounter() {
let count = $state(0);
function increment() {
count += 1;
}
return {
get count() { return count },
increment
};
}
const counter = createCounter();
</script>
<button onclick={increment}>
clicks: {count}
<button onclick={counter.increment}>
clicks: {counter.count}
</button>
Note that we're using a
get
property in the returned object, so thatcounter.count
always refers to the current value rather than the value at the time thecreateCounter
function was called.As a corollary,
const { count, increment } = createCounter()
won't work. That's because in JavaScript, destructured declarations are evaluated at the time of destructuring — in other words,count
will never update.
We can also extract that function out into a separate .svelte.js
or .svelte.ts
module...
ts
export functioncreateCounter () {letcount =$state (0);functionincrement () {count += 1;}return {getcount () {returncount ;},increment };}
...and import it into our component:
<script>
import { createCounter } from './counter.svelte.js';
function createCounter() {...}
const counter = createCounter();
</script>
<button onclick={counter.increment}>
clicks: {counter.count}
</button>
See this example in the playground.
Stores equivalentpermalink
In Svelte 4, the way you'd do this is by creating a custom store, perhaps like this:
ts
import {writable } from 'svelte/store';export functioncreateCounter () {const {subscribe ,update } =writable (0);functionincrement () {update ((count ) =>count + 1);}return {subscribe ,increment };}
Back in the component, we retrieve the store value by prefixing its name with $
:
<script>
import { createCounter } from './counter.js';
const counter = createCounter();
</script>
<button onclick={counter.increment}>
clicks: {counter.count}
clicks: {$counter}
</button>
The store approach has some significant drawbacks. A counter is just about the simplest custom store we could create, and yet we have to completely change how the code is written — importing writable
, understanding its API, grabbing references to subscribe
and update
, changing the implementation of increment
from count += 1
to something far more cryptic, and prefixing the store name with a $
to retrieve its value. That's a lot of stuff you need to understand.
With runes, we just copy the existing code into a new function.
Gotchaspermalink
Reactivity doesn't magically cross function boundaries. In other words, replacing the get
property with a regular property wouldn't work...
export function createCounter() {
let count = $state(0);
function increment() {
count += 1;
}
return {
get count() { return count },
count,
increment
};
}
...because the value of count
in the returned object would always be 0
. Using the $state
rune doesn't change that fact — it simply means that when you do read count
(whether via a get
property or a normal function) inside your template or inside an effect, Svelte knows what to update when count
changes.