Svelte 5 and SvelteKit development expertise including runes-based reactivity ($state, $derived, $effect, $props), file-based routing, form actions, remote functions with Zod validation, and best practices. Use when working with Svelte 5 or SvelteKit projects, creating components, setting up routes, or implementing forms and data loading.
Complete guide for modern Svelte 5 and SvelteKit development with runes, routing, and best practices.
Svelte 5 introduces runes - compiler symbols prefixed with $ that provide explicit, universal reactivity. They work in .svelte, .svelte.js, and .svelte.ts files.
$state - Reactive State<script>
// Basic state
let count = $state(0);
// Object state (deeply reactive)
let user = $state({
name: 'Alice',
age: 30
});
// Array state
let items = $state([1, 2, 3]);
</script>
<button onclick={() => count++}>
Count: {count}
</button>
Best Practices:
$state for all reactive variables$state.raw()$derived - Computed Values<script>
let count = $state(0);
let doubled = $derived(count * 2);
// For complex computations, use $derived.by
let total = $derived.by(() => {
let sum = 0;
for (const n of numbers) {
sum += n;
}
return sum;
});
</script>
Best Practices:
$derived for computed values, NOT $effect$derived - will error$effect - Side Effects<script>
let count = $state(0);
$effect(() => {
console.log(`Count is now ${count}`);
// Cleanup function (optional)
return () => {
console.log('Cleanup before next run');
};
});
// Runs before DOM updates
$effect.pre(() => {
// Code here runs before DOM updates
});
</script>
Best Practices:
$effect for side effects ONLY (DOM manipulation, logging, external APIs)$derived for computing valuesCommon Anti-patterns to AVOID:
// ❌ BAD: Using $effect to compute values
$effect(() => {
doubled = count * 2; // Should be $derived
});
// ✅ GOOD: Use $derived for computation
let doubled = $derived(count * 2);
// ❌ BAD: State changes in $derived
let bad = $derived(count++); // Will error
// ✅ GOOD: Pure computation
let good = $derived(count + 1);
$props - Component Props<script>
// Destructure props
let { name, age = 18, optional } = $props();
// With TypeScript
interface Props {
name: string;
age?: number;
}
let { name, age = 18 }: Props = $props();
// Bindable props
let { value = $bindable(0) } = $props();
</script>
Best Practices:
$props() instead of export let (Svelte 4 syntax)$bindable() for two-way binding<script>
let count = $state(0);
// Events are now just props
let { onclick } = $props();
function handleClick() {
count++;
onclick?.(); // Call parent handler if provided
}
</script>
<button {onclick}>Click me</button>
<!-- Parent component -->
<MyButton onclick={() => console.log('clicked')} />
Best Practices:
createEventDispatcher()if (onclick) { ... }<script>
let items = $state(['a', 'b', 'c']);
</script>
{#snippet card(title, content)}
<div class="card">
<h3>{title}</h3>
<p>{content}</p>
</div>
{/snippet}
{#each items as item}
{@render card(item, `Content for ${item}`)}
{/each}
<script>
import { onMount, onDestroy } from 'svelte';
// Still available and recommended for lifecycle
onMount(() => {
console.log('Component mounted');
return () => {
console.log('Cleanup on unmount');
};
});
// Note: beforeUpdate and afterUpdate are deprecated
// Use $effect instead when you need reactive cleanup
</script>
src/routes/
├── +page.svelte # / route
├── +layout.svelte # Root layout
├── +layout.server.ts # Server layout load
├── about/
│ └── +page.svelte # /about
├── blog/
│ ├── +page.svelte # /blog
│ ├── [slug]/
│ │ └── +page.svelte # /blog/[slug]
│ └── [...catchall]/
│ └── +page.svelte # /blog/* (catch-all)
├── (authed)/ # Route group (no URL segment)
│ ├── +layout.svelte # Shared layout
│ ├── dashboard/
│ │ └── +page.svelte # /dashboard
│ └── settings/
│ └── +page.svelte # /settings
└── api/
└── posts/
└── +server.ts # API endpoint
Key Files:
+page.svelte - Page component+page.ts - Universal load function (runs on server and client)+page.server.ts - Server-only load function+layout.svelte - Layout wrapper+layout.ts / +layout.server.ts - Layout data loading+server.ts - API endpoints (GET, POST, etc.)+error.svelte - Error boundary// +page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
// Runs only on server
// Has access to: database, environment variables, cookies
const post = await db.getPost(params.slug);
return {
post
};
};
// +page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch, data }) => {
// Runs on server AND client
// Can access parent data via 'data'
// Use for client-safe operations
const response = await fetch(`/api/posts/${params.slug}`);
const post = await response.json();
return {
post
};
};
Best Practices:
+page.server.ts for database access, secrets, server-only logic+page.ts for API calls, client-safe operationsdata paramawait parent()fetch for automatic request tracking// +page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
// Default action
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
if (!email) {
return fail(400, { email, missing: true });
}
// Process data...
redirect(303, '/success');
},
// Named action
login: async ({ request }) => {
const data = await request.formData();
// Handle login...
return { success: true };
}
};
<!-- +page.svelte -->
<script>
import { enhance } from '$app/forms';
let { data, form } = $props();
</script>
<!-- Default action -->
<form method="POST" use:enhance>
<input name="email" />
{#if form?.missing}
<p class="error">Email is required</p>
{/if}
<button>Submit</button>
</form>
<!-- Named action -->
<form method="POST" action="?/login" use:enhance>
<input name="username" />
<button>Login</button>
</form>
// data.remote.ts
import { query, form } from '@sveltejs/kit';
import { z } from 'zod';
// Remote query
export const getPosts = query(async () => {
return await db.posts.findMany();
});
// Remote form with Zod validation
const createPostSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(10, 'Content must be at least 10 characters')
});
export const createPost = form(createPostSchema, async (data) => {
const post = await db.posts.create(data);
// Refresh queries
getPosts.refresh();
return { success: true, post };
});
<!-- +page.svelte -->
<script>
import { createPost, getPosts } from './data.remote';
let posts = getPosts();
</script>
<form {...createPost}>
<input name="title" />
<textarea name="content"></textarea>
<button>Create</button>
</form>
{#each posts.data as post}
<article>{post.title}</article>
{/each}
Form Action Best Practices:
use:enhance for progressive enhancementfail()redirect() for navigation after successaction="?/actionName"form prop// +server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, params }) => {
const data = await fetchData(params.id);
if (!data) {
error(404, 'Not found');
}
return json(data);
};
export const POST: RequestHandler = async ({ request }) => {
const data = await request.json();
// Process...
return json({ success: true }, { status: 201 });
};
// +page.ts or +page.server.ts
export const prerender = true; // Static generation
export const ssr = false; // Disable SSR (SPA mode)
export const csr = true; // Enable client-side rendering
// stores.svelte.ts - Shared state with runes
export function createCounter(initial = 0) {
let count = $state(initial);
let doubled = $derived(count * 2);
return {
get count() { return count; },
get doubled() { return doubled; },
increment: () => count++,
decrement: () => count--
};
}
// Usage in components
import { createCounter } from './stores.svelte';
const counter = createCounter();
<script>
import { enhance } from '$app/forms';
let { form } = $props();
let loading = $state(false);
</script>
<form
method="POST"
use:enhance={() => {
loading = true;
return async ({ result, update }) => {
loading = false;
await update();
};
}}
>
<input name="email" />
{#if form?.errors}
<p class="error">{form.errors.email}</p>
{/if}
<button disabled={loading}>
{loading ? 'Submitting...' : 'Submit'}
</button>
</form>
<!-- +error.svelte -->
<script>
import { page } from '$app/state';
</script>
<h1>{page.status}</h1>
<p>{page.error.message}</p>
<script>
import { goto } from '$app/navigation';
function navigate() {
goto('/dashboard');
}
</script>
<!-- Use native <a> for links -->
<a href="/about">About</a>
export let → $props()let (reactive) → $state()$: statements → $derived() or $effect()createEventDispatcher → Props/callbacks<!-- Svelte 4 -->
<script>
export let name;
let doubled;
$: doubled = count * 2;
$: console.log(count);
</script>
<!-- Svelte 5 -->
<script>
let { name } = $props();
let doubled = $derived(count * 2);
$effect(() => console.log(count));
</script>
❌ Don't use $effect for computed values
// Bad
$effect(() => {
total = a + b;
});
// Good
let total = $derived(a + b);
❌ Don't mutate state inside $derived
// Bad
let bad = $derived(count++);
// Good
let good = $derived(count + 1);
❌ Don't use old Svelte 4 patterns in new code
// Bad
export let prop;
$: doubled = count * 2;
// Good
let { prop } = $props();
let doubled = $derived(count * 2);
❌ Don't forget to use .server files for sensitive operations
// Bad: +page.ts (runs on client!)
export const load = async () => {
const secret = process.env.SECRET_KEY; // Exposed to client!
};
// Good: +page.server.ts
export const load = async () => {
const secret = process.env.SECRET_KEY; // Server-only
};
$derived over $effect for computations - More efficient$state.raw() for non-reactive data - Avoid unnecessary trackingexport const prerender = true// Component with typed props
<script lang="ts">
interface Props {
title: string;
count?: number;
onClick?: () => void;
}
let { title, count = 0, onClick }: Props = $props();
// Derived with type inference
let doubled: number = $derived(count * 2);
</script>
// Load function types
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
// Fully typed
};
src/
├── lib/
│ ├── components/
│ │ ├── ui/ # Reusable UI components
│ │ └── features/ # Feature-specific components
│ ├── stores.svelte.ts # Shared reactive state
│ ├── utils/ # Utility functions
│ └── server/ # Server-only code
├── routes/
│ ├── +layout.svelte
│ ├── +page.svelte
│ └── ...
└── app.html
.svelte.js files$derived for values, $effect for side effects - Clear separation.server.ts files are server-only