From harness-claude
Guides SvelteKit state scopes: runes for component-local, context API for subtrees, module singletons for globals. Fixes SSR data leaks and prop-drilling.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Choose the right state scope in SvelteKit: component-local runes, context API for subtree isolation, and module-level state for true singletons
Guides SvelteKit development with Svelte 5 runes ($state, $derived, $effect), routing, form actions, load functions, SSR, and deployment best practices.
Implements Svelte stores pattern to share reactive state across component trees without prop-drilling, covering writable, readable, derived, and custom stores for Svelte 4/5 apps.
Enforces Svelte 5 best practices in SvelteKit: runes ($state, $derived, $effect), $props(), $bindable(), load functions, form actions, and SSR patterns to fix outdated Svelte 4 code.
Share bugs, ideas, or general feedback.
Choose the right state scope in SvelteKit: component-local runes, context API for subtree isolation, and module-level state for true singletons
Layer 1 — component-local state (runes):
$state directly. This is always the starting point:<script lang="ts">
let count = $state(0)
let open = $state(false)
</script>
Layer 2 — reactive class / .svelte.ts for shared reactive logic:
.svelte.ts file using $state and $derived. This works in Svelte 5 and is preferred over Svelte 4 stores:// lib/counter.svelte.ts
export class Counter {
count = $state(0);
doubled = $derived(this.count * 2);
increment() {
this.count++;
}
reset() {
this.count = 0;
}
}
// Singleton (app-wide):
export const counter = new Counter();
// Or factory (per-use):
export function createCounter() {
return new Counter();
}
Layer 3 — context API for subtree isolation:
setContext/getContext to share state within a component tree without prop-drilling. Context is scoped to the component that calls setContext and all its descendants:<!-- FormRoot.svelte — sets context -->
<script lang="ts">
import { setContext } from 'svelte'
const form = $state({ values: {}, errors: {} })
setContext('form', form)
</script>
<slot />
<!-- FormField.svelte — reads context -->
<script lang="ts">
import { getContext } from 'svelte'
const form = getContext<FormState>('form')
</script>
// lib/form-context.ts
import { getContext, setContext } from 'svelte';
const FORM_KEY = Symbol('form');
export function setFormContext(state: FormState) {
setContext(FORM_KEY, state);
}
export function getFormContext(): FormState {
const ctx = getContext<FormState>(FORM_KEY);
if (!ctx) throw new Error('getFormContext must be called inside a FormRoot component');
return ctx;
}
Layer 4 — module-level singletons (client-only):
// lib/auth.svelte.ts
let user = $state<User | null>(null);
export function getUser() {
return user;
}
export function setUser(u: User | null) {
user = u;
}
// WRONG — server-side singleton shared between ALL users:
let currentUser = $state<User | null>(null); // data leak!
// CORRECT — use locals (set in hooks.server.ts) for per-request server state
// CORRECT — use context API for per-component-tree state
SvelteKit page data as state source:
<script lang="ts">
import type { PageData } from './$types'
let { data }: { data: PageData } = $props()
// data.user is already reactive — no need to copy to $state
// Just use data.user directly in the template
</script>
State management decision tree:
Is the state used in only one component?
→ Yes → $state (rune)
Does it need to be shared across a component subtree?
→ Yes → Context API (setContext/getContext)
Does it need to be shared app-wide (no component boundary)?
→ Client-only? → Module-level $state in .svelte.ts
→ SSR-safe? → Server: locals / load data; Client: stores or context
Does it come from the server?
→ Use load functions + page data (not stores)
Why stores cause SSR issues:
Svelte stores are module-level singletons. In Node.js, modules are shared across all simultaneous requests. If a user's data is written to a store on request A, it can be visible to request B's render if B starts before A finishes. The fix: use event.locals in hooks.server.ts, passed through load functions.
Context API and SSR:
Context is per-render-tree on the server (each SSR request creates a fresh component tree), so it is SSR-safe. This makes it the right choice for per-request data in layouts and pages.
Derived state from page data:
If you need computed values based on page data, use $derived rather than storing derived values:
<script lang="ts">
let { data } = $props()
const totalPrice = $derived(
data.cartItems.reduce((sum, item) => sum + item.price, 0)
)
</script>
Persisting state across navigations:
SvelteKit re-runs load functions on navigation, replacing data. For client-side persistence across navigations, use module-level state or the browser's sessionStorage/localStorage.
https://kit.svelte.dev/docs/state-management