Skip to content

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:

bash
pnpm create svelte@latest my-app
cd my-app
bash
pnpm add @stateloom/core @stateloom/svelte
bash
npm install @stateloom/core @stateloom/svelte
bash
yarn add @stateloom/core @stateloom/svelte

Add a paradigm adapter:

bash
pnpm add @stateloom/store
bash
pnpm add @stateloom/atom
bash
pnpm add @stateloom/proxy

Basic Integration

Writable Signals with toWritable

Bridge StateLoom signals to Svelte's Writable store contract. Use toWritable for signals you need to write to:

typescript
// 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);
}
svelte
<!-- 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:

svelte
<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:

svelte
<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:

svelte
<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:

typescript
// 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);
svelte
<!-- 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

svelte
<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

typescript
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:

svelte
<!-- src/routes/+layout.svelte -->
<script>
  import { createScope } from '@stateloom/core';
  import { setScope } from '@stateloom/svelte';

  const scope = createScope();
  setScope(scope);
</script>

<slot />
svelte
<!-- 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:

typescript
// 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:

FeatureSvelte 4Svelte 5
$store syntax$count$ (auto-subscription)$count$ (same)
bind:valueWorks with toWritableWorks with toWritable
Runes ($state, $derived)N/AUse 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

Live Demo

Try the Svelte example directly in your browser:

Open in StackBlitz | Open Static Demo