Svelte 5 Best Practices
Contents
Svelte 5 Best Practices
Overview
Svelte 5 introduces Runes — a universal, fine-grained reactivity system that replaces the Svelte 4 compiler-driven reactivity. This page documents production-ready patterns for Svelte 5 (v5.0+).
Core Runes
$state — Reactive State
<script>
// Primitive — fine-grained
let count = $state(0);
// Object — deeply reactive (proxied)
let user = $state({ name: 'Ada', age: 36 });
// Array — deeply reactive
let items = $state(['a', 'b', 'c']);
// Raw — no proxy, only reassignment triggers updates
let rawData = $state.raw(largeApiResponse);
</script>
Key rules:
- Only use
$statefor values that should trigger reactive updates - Objects/arrays are deeply reactive — mutation triggers updates (with proxy overhead)
- Use
$state.rawfor large objects only ever reassigned, never mutated $statein.svelte.js/.tsmodules creates shared state across components
$derived — Computed Values
<script>
let count = $state(0);
// Simple derivation — expression (preferred)
let doubled = $derived(count * 2);
// Complex derivation — function
let expensive = $derived.by(() => {
return heavyComputation(count);
});
// Derived objects/arrays — NOT deeply reactive by default
let summary = $derived.by(() => ({
total: items.length,
active: items.filter(i => i.active).length
}));
</script>
Rules:
- Use
$derived(expression) over$derived.by(function) when possible $derived.byfor complex logic, async, or when you need$stateinside- Derived values are writable — you can assign to them, but they re-evaluate when dependencies change
- Derived objects/arrays are returned as-is (not proxied)
$effect — Side Effects
<script>
let count = $state(0);
// Avoid: syncing state to external libs
$effect(() => {
d3.select('#chart').datum(data).call(chart);
});
// Prefer: {@attach} for DOM libs
// <div {@attach}={d3Chart} />
// Avoid: running code on user interaction
$effect(() => {
if (count > 10) analytics.track('milestone');
});
// Prefer: event handler
function increment() {
count++;
if (count > 10) analytics.track('milestone');
}
// Debugging: trace reactivity
$effect(() => {
$inspect.trace('count-effect');
console.log(count);
});
</script>
Anti-patterns to avoid:
- Updating state inside effects (causes infinite loops)
- Using effects for event listeners (use
<svelte:window onkeydown={...}>) - Wrapping effects in
if (browser)— effects don’t run on server
$props — Component Props
<script>
// Destructure with defaults
let {
title = 'Default',
count = 0,
onClick
} = $props();
// Props are reactive — use $derived for computed values
let style = $derived(count > 10 ? 'danger' : 'normal');
// Bindable props
let { bind:value = '' } = $props();
</script>
Rules:
- Treat props as mutable — they can change from parent
- Always use
$derivedfor values computed from props - Use
bind:on parent for two-way binding:<Child bind:value={val} />
$inspect — Debugging
<script>
// Trace what triggered an effect/derived
$effect(() => {
$inspect.trace('user-sync');
syncToBackend(user);
});
// Log with label
$inspect('user-change', user);
</script>
Snippets — Reusable Markup
{#snippet button(label, variant='primary')}
<button class="btn btn-{variant}">{label}</button>
{/snippet}
{#snippet card(title, children)}
<article class="card">
<h3>{title}</h3>
{@render children()}
</article>
{/snippet}
<!-- Usage -->
{@render button('Save', 'success')}
{@render card('Title', () => <p>Content</p>)}
Key points:
- Snippets are declared in the template (not in
<script>) - Pass data via parameters, render with
{@render ...} - Can be passed as props:
<Component snippet={mySnippet} /> - Replace slots from Svelte 4 — more flexible, type-safe
Event Handlers
<!-- Direct handler -->
<button onclick={handleClick}>Click</button>
<!-- Shorthand -->
<button {onclick}>Click</button>
<!-- Spread (for wrapper components) -->
<button {...props}>Click</button>
<!-- Window/document events -->
<svelte:window onkeydown={handleKey} />
<svelte:document onvisibilitychange={handleVisibility} />
Never use onMount or $effect for window/document listeners.
{@attach} — External Library Integration
<script>
import * as d3 from 'd3';
let chart = $state(null);
function d3Chart(element) {
const sel = d3.select(element);
// ... setup chart
return () => { /* cleanup */ };
}
</script>
<div {@attach}={d3Chart} />
Cleanup function runs when element is destroyed.
{@const} — Template Constants
{#each items as item}
{@const index = items.indexOf(item)}
{@const isEven = index % 2 === 0}
<div class={isEven ? 'even' : 'odd'}>
{item.name} (#{index})
</div>
{/each}
Performance Patterns
Use $state.raw for Large Data
<script>
// API response — never mutated, only replaced
let posts = $state.raw([]);
async function load() {
posts = await fetchPosts(); // reassignment triggers update
}
</script>
Avoid Unnecessary Reactivity
<script>
// NOT reactive — plain variable
const apiBase = 'https://api.example.com';
// Only reactive if used in template/effect/derived
let count = $state(0);
</script>
Memoize Expensive Derivations
<script>
let items = $state([]);
// $derived.by caches until dependencies change
let sorted = $derived.by(() =>
[...items].sort((a, b) => a.name.localeCompare(b.name))
);
</script>
TypeScript
<script lang="ts">
interface User { name: string; age: number; }
let user = $state<User>({ name: '', age: 0 });
let { name = 'Guest' }: { name?: string } = $props();
// Snippet types
let { renderRow }: { renderRow: Snippet<[User]> } = $props();
</script>
Migration from Svelte 4
| Svelte 4 | Svelte 5 |
|---|---|
let count = 0; | let count = $state(0); |
$: doubled = count * 2; | let doubled = $derived(count * 2); |
export let title; | let { title } = $props(); |
onMount(() => {...}) | $effect(() => {...}) (runs on mount) |
<slot> | {@render children()} via snippet |
$$props / $$restProps | Use $props() rest: let { ...rest } = $props() |
on:click | onclick (no on: prefix) |
createEventDispatcher | Callback props: let { onClick } = $props() |
setContext / getContext | Still works, but prefer shared $state modules |
Related Topics
Evolution Notes
Content last updated: 2026-06-05 Next review: 2026-06-12