Best Practices for Working with Svelte
If you’ve moved past the basics of Svelte and started building real applications, you’ve probably noticed that the official docs cover what things do but not always when or why to use them. This article focuses on Svelte 5 best practices that improve maintainability, performance, and clarity in production code, assuming you already understand how components and reactivity work.
Key Takeaways
- Use
$stateonly when a value drives UI updates, and reach for$state.rawwhen you’re replacing values rather than mutating them. - Prefer
$derivedover$effectfor computed values; reserve$effectfor syncing with external systems. - Avoid module-level state in SSR environments. Use Svelte’s context API with class-based
$statefor type-safe, request-scoped shared state. - In SvelteKit, use
+page.server.jsfor server-side page data and+server.jsfor standalone API endpoints. - Adopt modern Svelte 5 syntax (
onclick,{#snippet},$props()) instead of legacy patterns in new code.
Svelte 5 Runes: Use Them Precisely
Svelte 5 runes are the primary reactivity model, and using them correctly matters more than using them everywhere.
Only reach for $state when a variable needs to drive UI updates. Plain variables are cheaper and clearer for everything else.
When your state is a large object or array that gets replaced rather than mutated, use $state.raw instead:
// ❌ Unnecessary proxy overhead for API data you'll only reassign
let users = $state(await fetchUsers());
// ✅ No proxy cost when you're replacing, not mutating
let users = $state.raw(await fetchUsers());
Use $state when you need to mutate nested properties directly (like cart.items[0].quantity++). Use $state.raw when you’re swapping the whole value.
Prefer $derived Over $effect for Computed Values
This is one of the most common mistakes in modern Svelte development:
let num = $state(0);
// ❌ Avoid — creates an unnecessary side effect
let square = $state(0);
$effect(() => { square = num * num; });
// ✅ Correct — declarative and dependency-tracked
let square = $derived(num * num);
$effect is an escape hatch. Reserve it for syncing with external systems (like D3), and consider {@attach} for DOM-level integrations where it fits naturally.
Treat Props as Dynamic
Values derived from props should use $derived, not plain assignment:
let { type } = $props();
// ✅ Stays in sync when `type` changes
let color = $derived(type === 'danger' ? 'red' : 'green');
Discover how at OpenReplay.com.
Type-Safe Context Over Shared Modules
For state shared across a component subtree, prefer Svelte’s context API over module-level state. Module state persists across requests in SSR environments, which can cause data to leak between users.
The modern pattern uses a class with $state fields:
// lib/theme.svelte.ts
import { getContext, setContext } from 'svelte';
class ThemeContext {
current = $state('light');
toggle() {
this.current = this.current === 'light' ? 'dark' : 'light';
}
}
const KEY = Symbol('theme');
export const setTheme = () => setContext(KEY, new ThemeContext());
export const getTheme = () => getContext<ThemeContext>(KEY);
This gives you type safety, reactive state, and proper SSR scoping in one pattern.
SvelteKit Data Loading: Choosing the Right Pattern
A common point of confusion in SvelteKit is when to use +page.server.js versus +server.js:
| Scenario | Use |
|---|---|
| Fetching data for a page with SSR or server-only access | +page.server.js with load() |
| Building an API endpoint for external use | +server.js |
| Client-only data after hydration | onMount + fetch |
For page data that needs server access, secrets, or SSR, +page.server.js is usually the right default. It runs server-side, keeps secrets out of the client, and integrates cleanly with SvelteKit’s form actions for progressive enhancement.
Small Practical Wins
Keyed {#each} blocks prevent subtle DOM recycling bugs. Always key by a stable unique ID, never by index.
$inspect.trace is underused for debugging reactivity. Drop it at the top of any $effect or $derived.by to see exactly which dependency triggered a re-run.
Snippets over slots for reusable markup chunks. Snippets compose better and can be passed as props, making component APIs cleaner.
Avoid legacy syntax in new code. Replace on:click with onclick, <slot> with {#snippet}, and export let with $props(). These patterns align with modern Svelte 5 conventions and current compiler behavior.
Conclusion
Svelte 5 rewards restraint. The more precisely you scope reactivity, using $state only where needed, $derived instead of $effect, and context instead of module globals, the more predictable and performant your application becomes. Start with the simplest reactive primitive that solves the problem, and only reach for more powerful tools when the simpler ones genuinely fall short.
FAQs
Use $state.raw when you plan to replace the entire value rather than mutate parts of it, such as when storing fetched API responses or large arrays you reassign wholesale. It skips the reactive proxy overhead, which improves performance for large data sets. Use plain $state when you need fine-grained reactivity for nested mutations like updating an item inside an array.
$derived is declarative, automatically tracks its dependencies, and produces a read-only value that stays in sync. $effect runs imperatively as a side effect and is harder to reason about, since it can introduce timing bugs and unnecessary re-runs. Reserve $effect for synchronizing with systems outside Svelte's reactivity, such as third-party libraries, canvas APIs, or manual DOM work.
Not in SSR environments like SvelteKit. Module-level state is shared across every request the server handles, which can leak one user's data into another user's session. Use Svelte's context API with setContext and getContext, ideally backed by a class containing $state fields. This scopes the state per request and per component tree while preserving reactivity and type safety.
Use +page.server.js when the data belongs to a specific page and benefits from SSR, SEO, server-side access, or form actions. Use +server.js when you need a standalone HTTP endpoint, such as a JSON API consumed by external clients, webhooks, or non-page fetch calls. If the data is only ever rendered by one page and needs server-side capabilities, +page.server.js is usually the better default.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.