Mastering Svelte 5 & SvelteKit 2: A Guide to the Future
Svelte 5 and SvelteKit 2 represent a monumental leap forward for the Svelte ecosystem. The core principles of simplicity and performance remain, but they are now supercharged with a more powerful, explicit, and scalable reactivity model called Runes. This guide covers the essential updates you need to know to build next-generation web applications.
Svelte 5: A New Era of Reactivity with Runes runes
The biggest change in Svelte 5 is the introduction of Runes, a set of special symbols that provide fine-grained, explicit control over reactivity. This moves away from the compiler's "magic" and gives developers more clarity and power.
$state: The Foundation of Reactivity
The $state rune is the new foundation for declaring reactive state. It replaces the simple let declarations that were automatically made reactive by the compiler in Svelte 4.
Svelte 4 (Implicit):
<script>
let count = 0;
function increment() {
count += 1; // The compiler makes this update the DOM
}
</script>
<button on:click={increment}>
Clicks: {count}
</button>
Svelte 5 (Explicit with $state):
<script>
let count = $state(0);
function increment() {
count += 1; // This is reactive because `count` was created with $state
}
</script>
<button onclick={increment}>
Clicks: {count}
</button>
$state works seamlessly with objects and arrays, providing deep reactivity without any extra work.
$derived: Clear & Performant Computed Values
The $derived rune is used to create values that are calculated from other pieces of state. It's a more explicit and often more performant replacement for the reactive $: statements from Svelte 4.
Svelte 4 (Reactive $: ):
<script>
let count = 0;
$: doubled = count * 2;
</script>
<p>{count} doubled is {doubled}</p>
Svelte 5 (Explicit with $derived):
<script>
let count = $state(0);
let doubled = $derived(count * 2);
</script>
<p>{count} doubled is {doubled}</p>
This makes dependencies crystal clear and allows Svelte to be smarter about when it needs to recalculate values.
$effect: Managing Side Effects
The $effect rune is the new way to run side effects—code that needs to execute in response to state changes but doesn't produce a value to be rendered (e.g., logging, saving to localStorage, or making API calls).
Svelte 4 (Reactive Block $: {}):
<script>
import { onMount } from 'svelte';
let count = 0;
let firstRender = true;
onMount(() => firstRender = false);
// This runs on mount AND when `count` changes
$: if (!firstRender) {
console.log(`The count is now \${count}`);
}
</script>
Svelte 5 (Explicit with $effect):
<script>
let count = $state(0);
// This runs after the first render and whenever `count` changes
$effect(() => {
console.log(`The count is now \${count}`);
});
</script>
$effect is cleaner and more intuitive, automatically tracking its dependencies (count in this case) and re-running when they change.
$props: A Modern Way to Define Props
Component properties are no longer defined with export let. Svelte 5 introduces the $props rune, which makes it immediately obvious which variables are passed in from a parent.
Parent Component:
<script>
import UserProfile from './UserProfile.svelte';
let currentUser = { name: 'Alex', role: 'Admin' };
</script>
<UserProfile user={currentUser} />
Child Component (Svelte 4):
<script>
export let user;
</script>
<h1>{user.name}</h1>
<p>Role: {user.role}</p>
Child Component (Svelte 5):
<script>
let { user } = $props();
</script>
<h1>{user.name}</h1>
<p>Role: {user.role}</p>
Snippets: The Evolution of Slots
Snippets are a more powerful and flexible replacement for slots. They can be passed as arguments and can even accept their own parameters.
Component accepting a Snippet (Card.svelte):
<script>
let { title, content } = $props();
</script>
<div class="card">
<div class="card-title">{title}</div>
<div class="card-content">
\{@render content({ style: 'italic' })}
</div>
</div>
Parent Component using the Snippet (App.svelte):
<script>
import Card from './Card.svelte';
</script>
<Card title="My Card">
{#snippet content(attrs)}
<p style="font-style: {attrs.style};">
This is some content passed as a snippet!
</p>
{/snippet}
</Card>
SvelteKit 2: Enhanced Application Building
SvelteKit 2 refines its core architecture, especially around data loading and form handling, to create a more robust and developer-friendly experience.
Robust Form Handling with Actions
SvelteKit Actions provide a formalized, server-side-only way to handle form submissions and data mutations. This is the foundation for progressive enhancement.
Server Logic (src/routes/login/+page.server.ts):
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
// This named action handles the login form submission
login: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) {
return fail(400, { email, missing: true });
}
// Authenticate user...
const user = { email: email as string, name: 'Test User' };
cookies.set('sessionid', 'your-session-token', { path: '/' });
// Redirect on success
throw redirect(303, '/dashboard');
}
};
Frontend Form (src/routes/login/+page.svelte):
The formaction attribute targets the login action specifically.
<script>
let { form } = $props(); // Receives data back from the action on failure
</script>
<form method="POST" action="?/login">
<label>
Email
<input name="email" type="email" value={form?.email ?? ''} />
</label>
{#if form?.missing}
<p class="error">Email and password are required.</p>
{/if}
<label>
Password
<input name="password" type="password" />
</label>
<button type="submit">Log in</button>
</form>
Progressive Enhancement with use:enhance
To provide a modern, app-like experience without a page reload, you can use the use:enhance directive. It progressively enhances the standard HTML form, intercepting the submission and updating the page with the result.
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let loading = false;
</script>
<form
method="POST"
action="?/login"
use:enhance={() => {
loading = true;
return async ({ update }) => {
await update();
loading = false;
};
\}\}
>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Log in'}
</button>
</form>