76 lines | 4.0 KB

SvelteKit (Svelte 5) — Claude Code rules

You are working in a SvelteKit app using Svelte 5 with runes. Prefer runes over the legacy Svelte 3/4 reactivity model. Stick to the conventions below.

Reactivity (runes)

  • Declare reactive state with $state(...). The value is a plain value you update like any variable — arrays/objects become a deeply reactive proxy, so mutating (arr.push(x), obj.foo = 1) triggers updates.
  • Use $state.raw(...) when you do not want deep reactivity — raw state can only be reassigned, never mutated.
  • Pass reactive proxies to non-Svelte libraries via $state.snapshot(value).
  • Compute values with $derived(expr), or $derived.by(() => { ... }) for multi-statement logic. The expression must be free of side-effects — Svelte disallows state changes (e.g. count++) inside it.
  • Runes are compiler keywords, not functions: don't import them, assign them to variables, or pass them as arguments.

Don't use legacy syntax

  • Don't use export let for props — use $props().
  • Don't use $: reactive statements — use $derived (for values) or $effect (for side-effects).
  • Don't write Svelte stores (writable/readable) for component-local state when runes do the job.

Effects

  • $effect(() => { ... }) runs after the component mounts and re-runs (batched, in a microtask) after the state it synchronously reads changes. Values read after await or inside setTimeout are not tracked.
  • $effect.pre(...) runs before the DOM updates (e.g. measure-before-paint).
  • $effect is an escape hatch for analytics, DOM manipulation, and external sync — not for deriving state. Don't use an effect to copy one piece of state into another; use $derived instead.
  • Don't read and write the same state in one effect (infinite loop); if unavoidable, wrap the write in untrack(...).

Components & props

  • Read props with $props(): let { a, b = 'default', ...rest } = $props();. Rename with let { class: className } = $props();.
  • Props are read-only — don't mutate props. Communicate up via callback props, or share a value explicitly with $bindable() for two-way binding.
  • +layout.svelte must render its children with {@render children()}.

Routing & data

  • Routes are filesystem-based under src/routes. A directory = a URL segment; [slug] is a dynamic param, [...rest] a rest param, [[optional]] an optional param.
  • +page.svelte renders a page and receives loaded data via the data prop (typed PageData from ./$types). +layout.svelte wraps child routes; +error.svelte is the error boundary.
  • +server.js defines API endpoints — export GET/POST/PUT/PATCH/DELETE returning a Response.
  • Load data in load functions, not in components:
    • +page.server.js / +layout.server.js (server load) for DB/filesystem access, private env vars, cookies, and locals. Returned data must be devalue-serializable (JSON, Date, Map, Set, BigInt, etc.).
    • +page.js / +layout.js (universal load) runs on server then client; use for public API fetches and to return non-serializable values (e.g. component constructors).
  • Use the injected fetch inside load (credential inheritance, relative URLs, SSR inlining). Read params / url / route; await parent() for parent layout data.
  • Throw redirect(3xx, location) and error(4xx|5xx, message) from load. Declare custom invalidation deps with depends(id) and refresh via invalidate(id) / invalidateAll().

State management

  • Never put per-user/request state in module-level variables on the server — modules are shared across requests and users, so state leaks between them.
  • load functions must be pure: no side-effects, no writing to shared stores. Return data instead.
  • For shared state scoped to a request/tree, use setContext / getContext, not globals.
  • Persist navigation-relevant state (filters, sort) in the URL search params; use snapshots for ephemeral UI state that should survive history navigation.