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
getproperty in the returned object, so thatcounter.countalways refers to the current value rather than the value at the time thecreateCounterfunction 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,countwill never update.
We can also extract that function out into a separate .svelte.js or .svelte.ts module...
tsexport 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:
tsimport {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.