runes
keywords. start with $.
$state
reactive state, basically.
<script>
let count = $state(0);
</script>
<button onclick="{() => count++;}">clicks: {count}</button>
contrasting with useState in react
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onclick={()=>setCount(count+1);}>
clicks: {count}
</button>
)
}
react gives us a get() and set(). svelte does all that under the hood. if $state has an Array, or Object (through Object.create), its result is a deeply reactive state proxy. proxies, allow svelte to run code when properties are read or written, at granular level. only the properties that changed, and refreshes only on the dependencies. use $state.raw if need be otherwise. custom class instances are not proxied, it seems. instead variables can directly be $states instead.
class Todo {
done = $state(false);
constructor(text) {
this.text = $state(text);
}
reset() {
this.text = "";
this.done = false;
}
}
the compiler itself will transform done and text into get and set methods, so they're not enumerable.
$state.snapshot: takes a static snapshot of a deeply reactive $state proxy.
$state.eager: when state changes, may not immediately change if await-ed
then, use $state.eager. use sparingly, i presume.
state as functions
regular addition
function add(a: number, b: number) {
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!
state-ful thinking works in functions
function add(getA: () => number, getB: () => number) {
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
svelte states work by considering the input as states itself
function add(input: { a: number; b: number }) {
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
you can't (shouldn't) export states. like
export let count = $state(0);
instead, either
- export it as
export const counter = $state({
count: 0,
});
or don't export the state itself, and instead the functions you write for it. as
let count = $state(0);
export function getCount() {
return count;
}
export function increment() {
count += 1;
}
$derived
state based on another state. the argument must not have sideeffects
const count = $state(0);
const derived = $derived(count * 2);
(can't have something that changes it, like count++.)
can add arrow functions with $derived.by
<script>
let numbers = $state([1, 2, 3]);
let total = $derived.by(() => {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
});
</script>
<button onclick="{() => numbers.push(numbers.length+1)}">
{numbers.join(' + ')} = {total}
</button>
$derived(expression) is the equivalent of $derived.by(() => expression)
everything that's within the $derivedexpression is a dependency, and will become dirty if it's changed, causing it to be recalculated. to exempt, you can sayuntrack()
if you want to override "derive"ations, as in [[dev notes#optimistic UI|optimistic UI]]
$effect
side effects. runs when dependencies change. basically the place where you do things instead of compute things.
<script>
let count = $state(0);
$effect(() => {
console.log("count changed:", count);
});
</script>
runs once initially, then whenever count changes.
mental model:
$state→ data$derived→ computation$effect→ side‑effects
things you do here:
- logging
- DOM APIs
- network calls
- subscriptions
things you do not do here:
- compute values for rendering (that belongs in
$derived).
cleanup
return a function.
$effect(() => {
const id = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(id);
});
same lifecycle intuition as useEffect, minus dependency arrays. dependencies are inferred automatically by reads.
$effect.pre
runs before DOM updates instead of after. use when you need layout reads before paint.
$effect.pre(() => {
// measure layout before DOM updates
});
rare. if you don't know why you need it, you probably don't.
dependency tracking
svelte tracks reads, not declarations.
let a = $state(1);
let b = $state(2);
$effect(() => {
console.log(a);
});
changing b does nothing because it wasn't read.
reads inside called functions also count.
function logA() {
console.log(a);
}
$effect(() => {
logA();
});
compiler/runtime tracks the access dynamically.
untrack
sometimes you want to read state without subscribing.
import { untrack } from "svelte";
$effect(() => {
const value = untrack(() => count);
console.log(value);
});
use cases:
- optimistic UI
- comparing previous values
- avoiding infinite loops
basically: "look but don't react".
passing state around
states are just values with reactive semantics.
function double(x) {
return $derived(x * 2);
}
let count = $state(2);
let doubled = double(count);
this is just a random seed instead of an actual blog. something to put on the website. random obsidian notes, if anything. if you read this far o7, because why.