Back

Best Practices for Working with Svelte

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 $state only when a value drives UI updates, and reach for $state.raw when you’re replacing values rather than mutating them.
  • Prefer $derived over $effect for computed values; reserve $effect for syncing with external systems.
  • Avoid module-level state in SSR environments. Use Svelte’s context API with class-based $state for type-safe, request-scoped shared state.
  • In SvelteKit, use +page.server.js for server-side page data and +server.js for 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');

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:

ScenarioUse
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 hydrationonMount + 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.

OpenReplay