Quick Start with Svelte
valdres-svelte is the Svelte 5 (runes) adapter. It bridges valdres atoms and
selectors into reactive boxes you read with .current, plus a store-contract
bridge for $-syntax and a provider tier for SSR.
Install
npm install valdres valdres-svelte
Create your first atom
// store.ts
import { atom } from "valdres"
export const countAtom = atom(0)
Provide a store
Call setValdresContext once near the root so descendants share a store. With
no argument it creates a store({ batchUpdates: true }) for the component tree
— which on the server means one store per request, the pattern SvelteKit wants.
<!-- +layout.svelte -->
<script lang="ts">
import { setValdresContext } from "valdres-svelte"
let { children } = $props()
setValdresContext()
</script>
{@render children()}
Read and write with fromState
fromState returns a reactive box. For an atom, .current is readable and
writable, so bind:value and count.current++ just work. For a
read-modify-write it also has update(fn) (like svelte/store's
Writable.update) and a reset().
<script lang="ts">
import { fromState } from "valdres-svelte"
import { countAtom } from "./store"
const count = fromState(countAtom)
</script>
<p>Count: {count.current}</p>
<button onclick={() => count.current++}>Increment</button>
<button onclick={() => count.update(c => c + 1)}>Increment (updater)</button>
<button onclick={() => count.reset()}>Reset</button>
<!-- two-way binding writes straight back to the atom -->
<input type="number" bind:value={count.current} />
Pass an explicit store as the second argument (fromState(countAtom, store))
when you're outside component initialization — e.g. in plain .svelte.ts
module state. Without it, the box resolves the store from context, which is only
available during init.
A selector (or any read-only state) yields a box with a read-only .current:
<script lang="ts">
import { fromState } from "valdres-svelte"
import { selector } from "valdres"
import { countAtom } from "./store"
const doubled = selector(get => get(countAtom) * 2)
const double = fromState(doubled)
</script>
<p>Doubled: {double.current}</p>
$-syntax with toStore
For Svelte's store contract ($-prefix auto-subscription, bind:value={$count$}),
use toStore. An atom becomes a Writable; a selector becomes a read-only
Readable. The store argument is optional and falls back to context.
<script lang="ts">
import { toStore } from "valdres-svelte"
import { countAtom } from "./store"
const count$ = toStore(countAtom)
</script>
<p>Count: {$count$}</p>
<button onclick={() => ($count$ = 0)}>Zero</button>
Scoped state
scope creates a child store layered over the parent for a subtree, with an
optional initialize to seed it. It detaches automatically when the component
is destroyed. Descendants reading via fromState/toStore resolve the scoped
store.
<!-- CheckoutSection.svelte -->
<script lang="ts">
import { scope, fromState } from "valdres-svelte"
import { countAtom } from "./store"
scope("checkout", { initialize: () => [[countAtom, 100]] })
const count = fromState(countAtom) // reads the scoped value (100)
</script>
<p>Scoped count: {count.current}</p>
Transactions
transaction() returns a runner bound to the context store, captured at
component init. That capture is what makes it safe to call from an event
handler — Svelte throws lifecycle_outside_component if you call getContext
inside a handler.
<script lang="ts">
import { transaction } from "valdres-svelte"
import { countAtom } from "./store"
const txn = transaction()
const buyTwo = () =>
txn(({ get, set }) => set(countAtom, get(countAtom) + 2))
</script>
<button onclick={buyTwo}>Buy two</button>
Async selectors
A selector can return a promise. Core erases asyncness, so fromState(sel).current
is typed V | Promise<V> — consume it with Svelte's {#await} block:
<script lang="ts">
import { fromState } from "valdres-svelte"
import { selector } from "valdres"
const userSelector = selector(() =>
fetch("/api/me").then(r => r.json()),
)
const user = fromState(userSelector)
</script>
{#await user.current}
<p>Loading…</p>
{:then u}
<p>Hello {u.name}</p>
{:catch error}
<p>Failed: {error.message}</p>
{/await}
When you'd rather branch on flags than await, resourceState runs the
promise detection for you and returns { current, loading, error }:
<script lang="ts">
import { resourceState } from "valdres-svelte"
import { userSelector } from "./store"
const user = resourceState(userSelector)
</script>
{#if user.loading}
<p>Loading…</p>
{:else if user.error}
<p>Failed: {String(user.error)}</p>
{:else}
<p>Hello {user.current?.name}</p>
{/if}
SvelteKit (SSR)
Load data on the server, then map it to atoms with initialize in the root
layout. Because initialize runs on both the server render and the client
hydration, the atoms hold the right values in both passes — no custom
serializer needed.
// +layout.server.ts
export const load = async () => {
return { count: 7 } // plain, serializable
}
<!-- +layout.svelte -->
<script lang="ts">
import { setValdresContext } from "valdres-svelte"
import { countAtom } from "$lib/store"
let { data, children } = $props()
setValdresContext({ initialize: txn => [[countAtom, data.count]] })
</script>
{@render children()}
If your load data is itself a dehydrate(store) payload (e.g. produced by an
API that ran a server-side valdres store), pass it as hydrate instead — it's
applied as a standalone commit on the fresh store. Atoms carrying a codec schema
round-trip BigInt/Date/Map/Set over plain JSON automatically. When both
are given, initialize runs first so hydrated values win.
<script lang="ts">
import { setValdresContext } from "valdres-svelte"
let { data, children } = $props()
setValdresContext({ hydrate: data.state })
</script>
{@render children()}
Gotcha: lazy bootstrap
The reactive box subscribes lazily (it's built on Svelte's createSubscriber,
the same primitive behind MediaQuery). A valdres atom with an onMount
bootstrap — the @valdres/browser-* packages, for instance — only starts once
.current is read inside an effect (a template expression, $derived,
$effect). A component that reads the value solely from an event handler will
see the unbootstrapped default. Read it in the template, or via $-syntax with
toStore, to start the subscription. If you genuinely need it to start without
rendering it, force the subscription with a throwaway effect:
<script lang="ts">
import { fromState } from "valdres-svelte"
import { onlineAtom } from "$lib/state"
const online = fromState(onlineAtom)
$effect(() => void online.current) // starts the subscription eagerly
</script>
Next steps
- Core Concepts — atoms, selectors, families, stores
- Patterns & Recipes — real-world examples