Svelte
Integrate StateLoom with a Svelte 4 or 5 application, including SvelteKit SSR support.
Data Flow
Prerequisites
- Svelte 4+ or Svelte 5+
- Node.js 18+
- SvelteKit (optional, for SSR)
Project Setup
Scaffold a new Svelte + Vite project and install StateLoom:
pnpm create svelte@latest my-app
cd my-apppnpm add @stateloom/core @stateloom/sveltenpm install @stateloom/core @stateloom/svelteyarn add @stateloom/core @stateloom/svelteAdd a paradigm adapter:
pnpm add @stateloom/storepnpm add @stateloom/atompnpm add @stateloom/proxyBasic Integration
Writable Signals with toWritable
Bridge StateLoom signals to Svelte's Writable store contract. Use toWritable for signals you need to write to:
// src/lib/stores/counter.ts
import { signal, computed } from '@stateloom/core';
import { toReadable, toWritable } from '@stateloom/svelte';
export const count = signal(0);
export const doubled = computed(() => count.get() * 2);
// Svelte store bridges for $-syntax
export const count$ = toWritable(count);
export const doubled$ = toReadable(doubled);
export function increment() {
count.update((n) => n + 1);
}
export function decrement() {
count.update((n) => n - 1);
}
export function reset() {
count.set(0);
}<!-- src/lib/Counter.svelte -->
<script>
import { count$, doubled$, increment, decrement, reset } from './stores/counter';
</script>
<section>
<h2>Counter</h2>
<p>Count: <strong>{$count$}</strong></p>
<p>Doubled: <strong>{$doubled$}</strong></p>
<button on:click={decrement}>-</button>
<button on:click={reset}>Reset</button>
<button on:click={increment}>+</button>
</section>The $count$ syntax works because toWritable returns a Svelte Writable store. Svelte auto-subscribes and auto-unsubscribes.
Read-Only Values with toReadable
For computed values or any Subscribable<T>, use toReadable:
<script>
import { signal, computed } from '@stateloom/core';
import { toReadable, toWritable } from '@stateloom/svelte';
const count = signal(0);
const doubled = computed(() => count.get() * 2);
const count$ = toWritable(count);
const doubled$ = toReadable(doubled);
</script>
<input type="number" bind:value={$count$} />
<p>Doubled: {$doubled$}</p>Two-Way Bindings
toWritable supports Svelte's bind:value directive:
<script>
import { signal } from '@stateloom/core';
import { toWritable } from '@stateloom/svelte';
const name = signal('Alice');
const name$ = toWritable(name);
</script>
<input bind:value={$name$} />
<p>Hello, {$name$}!</p>Store Integration
Use stores with toReadable and computed selectors:
<script>
import { createStore } from '@stateloom/store';
import { computed } from '@stateloom/core';
import { toReadable } from '@stateloom/svelte';
const store = createStore((set) => ({
count: 0,
name: 'Alice',
increment: () => set((s) => ({ count: s.count + 1 })),
}));
// Select a slice via computed
const count = computed(() => store.getState().count);
const count$ = toReadable(count);
function increment() {
store.getState().increment();
}
</script>
<span>Count: {$count$}</span>
<button on:click={increment}>+</button>Atom Integration
Use atoms with toReadable and toWritable:
// src/lib/stores/user-atoms.ts
import { atom, derived } from '@stateloom/atom';
import { toWritable, toReadable } from '@stateloom/svelte';
export const nameAtom = atom('Alice');
export const ageAtom = atom(30);
export const summaryAtom = derived((get) => `${get(nameAtom)}, age ${String(get(ageAtom))}`);
export const name$ = toWritable(nameAtom);
export const age$ = toWritable(ageAtom);
export const summary$ = toReadable(summaryAtom);<!-- src/lib/UserProfile.svelte -->
<script>
import { name$, age$, summary$ } from './stores/user-atoms';
</script>
<section>
<h2>User Profile</h2>
<label>Name: <input bind:value={$name$} /></label>
<label>Age: <input type="number" bind:value={$age$} /></label>
<p>Summary: <strong>{$summary$}</strong></p>
</section>Patterns
Theme Toggle with Persistence
<script>
import { createStore } from '@stateloom/store';
import { persist } from '@stateloom/persist';
import { computed } from '@stateloom/core';
import { toReadable } from '@stateloom/svelte';
const themeStore = createStore(
(set) => ({
theme: 'light',
toggle: () =>
set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' })),
}),
{ middleware: [persist({ key: 'theme' })] },
);
const theme = computed(() => themeStore.getState().theme);
const theme$ = toReadable(theme);
function toggle() {
themeStore.getState().toggle();
}
</script>
<button on:click={toggle}>
Theme: {$theme$}
</button>Middleware Stack
import { createStore } from '@stateloom/store';
import { devtools, logger } from '@stateloom/devtools';
import { persist } from '@stateloom/persist';
import { broadcast } from '@stateloom/tab-sync';
const store = createStore(
(set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
}),
{
middleware: [
logger({ enabled: import.meta.env.DEV }),
devtools({ name: 'Counter', enabled: import.meta.env.DEV }),
persist({ key: 'counter' }),
broadcast({ channel: 'counter' }),
],
},
);SSR with SvelteKit
Scope Management
Use setScope and getScope for per-request state isolation:
<!-- src/routes/+layout.svelte -->
<script>
import { createScope } from '@stateloom/core';
import { setScope } from '@stateloom/svelte';
const scope = createScope();
setScope(scope);
</script>
<slot /><!-- src/routes/+page.svelte -->
<script>
import { getScope } from '@stateloom/svelte';
const scope = getScope();
// Use scope for SSR-safe state reads
</script>Server Load Functions
Initialize state in SvelteKit load functions:
// src/routes/+page.server.ts
import { createScope, runInScope, serializeScope, signal } from '@stateloom/core';
const pageData = signal<string | null>(null);
export async function load() {
const scope = createScope();
runInScope(scope, () => {
scope.set(pageData, 'Server-rendered content');
});
return {
scopeData: serializeScope(scope),
};
}Svelte 4 vs Svelte 5
The @stateloom/svelte adapter works with both Svelte 4 and 5:
| Feature | Svelte 4 | Svelte 5 |
|---|---|---|
$store syntax | $count$ (auto-subscription) | $count$ (same) |
bind:value | Works with toWritable | Works with toWritable |
Runes ($state, $derived) | N/A | Use alongside StateLoom |
Svelte 5 Runes
In Svelte 5, you can use StateLoom for shared/global state and Svelte's $state/$derived for local component state. The two systems work independently.
Tips
Direct Subscribe Contract
StateLoom's Subscribable<T> is structurally close to Svelte's store contract. The only gap is the immediate invocation on subscribe, which toReadable/toWritable handle automatically.
Don't Mix Reactivity Systems
Use StateLoom signals for shared state across components. Use Svelte's native reactivity (let, $:, or $state/$derived in Svelte 5) for local component state.
Example App
See the complete Svelte + Vite example for a working app that demonstrates all paradigms.
Next Steps
- Svelte API Reference -- Full adapter documentation
- Store API Reference -- Store patterns and middleware
- Getting Started -- Core concepts and paradigm comparison
Live Demo
Try the Svelte example directly in your browser: