Svelte 5 Patterns
Quick Guide: Runes make reactivity explicit and portable out of
.sveltefiles.$statefor values that change,$derivedfor everything computed from them,$effectonly for reaching outside the component. Snippets replace slots, callback props replacecreateEventDispatcher, andonclickreplaceson:click. The Svelte 4 forms still compile, so nothing flags them.
Detailed Resources:
- examples/core.md —
$state,$state.raw,$derived,$props,$bindable,$effect,$effect.pre - examples/snippets.md — children, named snippets as props, parameters, optional and recursive snippets
- examples/events.md — element events, callback props, forwarding, window events, composing handlers
- examples/advanced.md —
$inspect, context, shared state modules, class-based state,$state.snapshot,$state.eager - reference.md — rune cheat sheet, Svelte 4 → 5 migration table, decision trees, component template
Which path applies
- Inside a
.sveltecomponent — every rune is available, props arrive through$props(), and teardown belongs in the function an$effectreturns. - Inside a
.svelte.tsor.svelte.jsmodule —$stateand$derivedwork, but a reassigned export does not propagate to importers, because the binding is copied at import. Export an object or a class holding$statefields instead: examples/advanced.md.
Before writing Svelte code
Declare changing values with $state and compute from them with $derived. A $derived recomputes lazily and cannot fall out of step; the same value maintained by an $effect updates after the DOM has already painted the old one.
Pass composable markup as snippets — {#snippet} declares it, {@render} renders it. Snippets are typed, take parameters, and can be passed as props; <slot> did none of that.
Let a child notify its parent through a callback prop — onsave, onselect. The signature is checked at the call site, where a dispatched event's payload was not.
Reach for $state.raw() when a value is replaced wholesale rather than mutated. It skips the deep proxy, which is the whole cost on a large array that only ever gets reassigned.
Reach for createContext<T>() over setContext/getContext. It hands back a typed [get, set] pair with the key minted for you, so no consumer casts and no two libraries collide on a string key — examples/advanced.md has it.
Auto-detection: Svelte 5, runes, $state, $derived, $effect, $props, $bindable, $inspect, .svelte, .svelte.ts, {#snippet}, {@render}, Snippet, createContext, setContext, getContext, $state.raw, $state.snapshot, $state.eager, $derived.by, $effect.pre, ClassValue
Applies to:
- Component state, derived values and side effects with runes
- Props, defaults, rest props and two-way binding with
$bindable - Composition with snippets, including snippets passed as props
- Event handling and parent-child communication
- Context, shared state modules and class-based reactive state
Handled elsewhere:
- Styling — a
<style>block is scoped by the compiler, and which CSS approach fills it is not settled here - Routing, server-side loading and form submission — a meta-framework's concern, whichever one is in use
- Server-state caching and invalidation
- Test doubles for the network
Philosophy
Svelte 4 inferred reactivity from position: a let at the top level of a component was reactive, $: re-ran on assignment, and neither meant anything in a .ts file. Runes replace that with a marker on the value itself, so the same declaration behaves identically in a component, a module and a class field.
The ordering that follows is: $derived for anything computable, an event handler for anything a user triggers, and $effect only for what is genuinely outside the component — a canvas, a third-party widget, a subscription. An $effect that assigns to $state is a $derived written the long way round, and it runs after the DOM update rather than before it.
Core patterns
Pattern 1: $state
$state makes a value reactive and, for objects and arrays, deeply so — push and property assignment are both tracked, with no immutable update dance.
<script lang="ts">
let count = $state(0);
let todos = $state<Todo[]>([]);
function addTodo(text: string) {
todos.push({ done: false, text });
}
</script>
Full code: examples/core.md
Pattern 2: $derived
$derived takes an expression, $derived.by a function for anything longer. Both recompute only when a dependency actually changed.
<script lang="ts">
let doubled = $derived(count * 2);
let stats = $derived.by(() => ({
isEven: count % 2 === 0,
isPositive: count > 0,
}));
</script>
Full code: examples/core.md
Pattern 3: $props
Props are destructured out of $props() with defaults and rest, and typed by an interface.
<script lang="ts">
interface Props {
name: string;
role?: string;
class?: string;
}
let { name, role = 'member', ...rest }: Props = $props();
let initials = $derived(name.split(' ').map((n) => n[0]).join(''));
</script>
Destructuring $props() is the one place it is safe — the compiler keeps the bindings live. Destructuring a $state object does not.
Full code: examples/core.md
Pattern 4: $bindable
$bindable marks a prop the child may write back through bind:. Worth it for form primitives; for everything else a callback prop keeps the data flowing one way.
<!-- text-input.svelte -->
<script lang="ts">
let { value = $bindable(''), placeholder = '' }: Props = $props();
</script>
<input bind:value {placeholder} />
<!-- parent.svelte -->
<TextInput bind:value={searchQuery} placeholder="Search..." />
Full code: examples/core.md
Pattern 5: $effect
An effect reaches outside the component; the function it returns is the teardown, run before the next execution and on unmount.
<script lang="ts">
$effect(() => {
const timer = setTimeout(() => search(query), DEBOUNCE_MS);
return () => clearTimeout(timer);
});
</script>
Anything of the form $effect(() => { x = f(y) }) is a $derived. Anything triggered by a click is an event handler. Anything you wanted to log is $inspect.
Full code: examples/core.md
Pattern 6: Snippets
{#snippet} declares a block of markup and {@render} renders it. Content between a component's tags becomes its children snippet automatically; anything else is a prop typed Snippet.
<script lang="ts">
import type { Snippet } from 'svelte';
let { title, children, footer }: { title: string; children: Snippet; footer?: Snippet } = $props();
</script>
<h2>{title}</h2>
{@render children()}
{#if footer}{@render footer()}{/if}
Full code: examples/snippets.md
Pattern 7: Events
Element events are plain attributes. Component events are callback props, optional ones called with ?.().
<script lang="ts">
let { color, onchange, onreset }: Props = $props();
</script>
<button => onchange?.('red')}>Red</button>
{#if onreset}<button
Full code: examples/events.md
Red flags
Breaks at runtime:
- Destructuring a
$stateobject — the values are read once at destructure time and never again - Mutating a
$state.rawvalue — only reassignment is tracked, which is the trade it exists to make setContextcalled from an event handler or an$effect— context is only settable during component initialisation$effectcreated outside component or module initialisation — calling it from an event handler is a runtime error rather than a silent no-op;$effect.root()is how an effect scope gets opened by hand, and it hands back its own cleanup- State read after an
awaitinside an$effect— those reads are not tracked, so the effect never re-runs for them - A cleanup function returned from
$derived— only$effectruns one $effectrelied on during server rendering — it runs in the browser only
Surprising behaviour:
- Every Svelte 4 form still compiles:
export let,$:,<slot>,createEventDispatcher,on:click,<svelte:component this={X}>. Nothing warns, so a file can be half-migrated and look fine - A
$statevalue is a proxy, not the object you passed —$state.snapshot()before serialising or handing it to a library that compares identity - A
$derivedresult is not deeply reactive; only$statecreates the proxy - Fallback values in
$props()are not proxied either $effectruns after the DOM update —$effect.preis the hook for measuring before it$inspectcompiles to nothing in production, so it is a debugging tool rather than logging- Svelte 5 delegates some events at the root, which changes what
stopPropagation()reaches