title |
---|
$state |
The $state
rune allows you to create reactive state, which means that your UI reacts when it changes.
<script>
let count = $state(0);
</script>
<button onclick={() => count++}>
clicks: {count}
</button>
Unlike other frameworks you may have encountered, there is no API for interacting with state — count
is just a number, rather than an object or a function, and you can update it like you would update any other variable.
If $state
is used with an array or a simple object, the result is a deeply reactive state proxy. Proxies allow Svelte to run code when you read or write properties, including via methods like array.push(...)
, triggering granular updates.
[!NOTE] Classes like
Set
andMap
will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported fromsvelte/reactivity
.
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
let todos = $state([
{
done: false,
text: 'add more todos'
}
]);
...modifying an individual todo's property will trigger updates to anything in your UI that depends on that specific property:
let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
todos[0].done = !todos[0].done;
If you push a new object to the array, it will also be proxified:
let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
todos.push({
done: false,
text: 'eat lunch'
});
[!NOTE] When you update properties of proxies, the original object is not mutated.
Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:
let todos = [{ done: false, text: 'add more todos' }];
// ---cut---
let { done, text } = todos[0];
// this will not affect the value of `done`
todos[0].done = !todos[0].done;
You can also use $state
in class fields (whether public or private):
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
}
reset() {
this.text = '';
this.done = false;
}
}
[!NOTE] The compiler transforms
done
andtext
intoget
/set
methods on the class prototype referencing private fields. This means the properties are not enumerable.
When calling methods in JavaScript, the value of this
matters. This won't work, because this
inside the reset
method will be the <button>
rather than the Todo
:
<button onclick={todo.reset}>
reset
</button>
You can either use an inline function...
<button onclick=+++{() => todo.reset()}>+++
reset
</button>
...or use an arrow function in the class definition:
// @errors: 7006 2554
class Todo {
done = $state(false);
text = $state();
constructor(text) {
this.text = text;
}
+++reset = () => {+++
this.text = '';
this.done = false;
}
}
In cases where you don't want objects and arrays to be deeply reactive you can use $state.raw
.
State declared with $state.raw
cannot be mutated; it can only be reassigned. In other words, rather than assigning to a property of an object, or using an array method like push
, replace the object or array altogether if you'd like to update it:
let person = $state.raw({
name: 'Heraclitus',
age: 49
});
// this will have no effect
person.age += 1;
// this will work, because we're creating a new person
person = {
name: 'Heraclitus',
age: 50
};
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can contain reactive state (for example, a raw array of reactive objects).
To take a static snapshot of a deeply reactive $state
proxy, use $state.snapshot
:
<script>
let counter = $state({ count: 0 });
function onclick() {
// Will log `{ count: ... }` rather than `Proxy { ... }`
console.log($state.snapshot(counter));
}
</script>
This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as structuredClone
.
In the case that you aren't using a proxied $state
via use of $state.raw
or a class instance, you may need to tell Svelte a $state
has changed. You can do so via $state.invalidate
:
<script>
import Counter from 'external-class';
let counter = $state(new Counter());
function increment() {
counter.increment(); // `counter`'s internal state has changed, but Svelte doesn't know that yet
$state.invalidate(counter);
}
</script>
<button onclick={increment}>
Count is {counter.count}
</button>
$state.invalidate
can also be used with reactive class fields, and properties of $state
objects:
class Box {
value;
constructor(initial) {
this.value = initial;
}
}
class Counter {
count = $state(new Box(0));
increment() {
this.count.value += 1;
$state.invalidate(this.count);
}
}
let counter = $state({count: new Box(0)});
function increment() {
counter.count.value += 1;
$state.invalidate(counter.count);
}
JavaScript is a pass-by-value language — when you call a function, the arguments are the values rather than the variables. In other words:
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {number} a
* @param {number} b
*/
function add(a, b) {
return a + b;
}
let a = 1;
let b = 2;
let total = add(a, b);
console.log(total); // 3
a = 3;
b = 4;
console.log(total); // still 3!
If add
wanted to have access to the current values of a
and b
, and to return the current total
value, you would need to use functions instead:
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {() => number} getA
* @param {() => number} getB
*/
function add(+++getA, getB+++) {
return +++() => getA() + getB()+++;
}
let a = 1;
let b = 2;
let total = add+++(() => a, () => b)+++;
console.log(+++total()+++); // 3
a = 3;
b = 4;
console.log(+++total()+++); // 7
State in Svelte is no different — when you reference something declared with the $state
rune...
let a = +++$state(1)+++;
let b = +++$state(2)+++;
...you're accessing its current value.
Note that 'functions' is broad — it encompasses properties of proxies and get
/set
properties...
/// file: index.js
// @filename: index.js
// ---cut---
/**
* @param {{ a: number, b: number }} input
*/
function add(input) {
return {
get value() {
return input.a + input.b;
}
};
}
let input = $state({ a: 1, b: 2 });
let total = add(input);
console.log(total.value); // 3
input.a = 3;
input.b = 4;
console.log(total.value); // 7
...though if you find yourself writing code like that, consider using classes instead.
You can declare state in .svelte.js
and .svelte.ts
files, but you can only export that state if it's not directly reassigned. In other words you can't do this:
/// file: state.svelte.js
export let count = $state(0);
export function increment() {
count += 1;
}
That's because every reference to count
is transformed by the Svelte compiler — the code above is roughly equivalent to this:
/// file: state.svelte.js (compiler output)
// @filename: index.ts
interface Signal<T> {
value: T;
}
interface Svelte {
state<T>(value?: T): Signal<T>;
get<T>(source: Signal<T>): T;
set<T>(source: Signal<T>, value: T): void;
}
declare const $: Svelte;
// ---cut---
export let count = $.state(0);
export function increment() {
$.set(count, $.get(count) + 1);
}
[!NOTE] You can see the code Svelte generates by clicking the 'JS Output' tab in the playground.
Since the compiler only operates on one file at a time, if another file imports count
Svelte doesn't know that it needs to wrap each reference in $.get
and $.set
:
// @filename: state.svelte.js
export let count = 0;
// @filename: index.js
// ---cut---
import { count } from './state.svelte.js';
console.log(typeof count); // 'object', not 'number'
This leaves you with two options for sharing state between modules — either don't reassign it...
// This is allowed — since we're updating
// `counter.count` rather than `counter`,
// Svelte doesn't wrap it in `$.state`
export const counter = $state({
count: 0
});
export function increment() {
counter.count += 1;
}
...or don't directly export it:
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}