Svelte 5 bindings for Effect Atom (effect/reactivity): hooks with a reactive current, async atoms you can await in markup, and server rendering with hydration.
Documentation · Installation · API reference · Changelog
This is a community project, not part of Effect: see Project status.
Effect Atom keeps state in atoms: small reactive values that hold plain data, derive from other atoms, or run an Effect or a Stream. The API follows @effect/atom-react, so if you have used atoms in React, only the hooks change.
- Hooks with a reactive
current, Svelte's convention for reactive values. Read it in markup, assign to it, orbind:to it. See Reading and writing. - Async atoms you can
await.useAtomSuspenseanduseAtomResultbuild on Svelte's async support, so pending and failed states go through<svelte:boundary>. See Suspense. - Server rendering and hydration. Each request gets its own registry, and the results of serializable atoms travel to the browser, which starts from them instead of running the effects again. See Server rendering and Hydration.
- Effect services in components.
AtomRpcandAtomHttpApiturn an Effect RPC group orHttpApiinto atoms for queries and mutations. See RPC and HTTP API. - Scoped atoms for one atom per component subtree, and SvelteKit
handleErrorhooks that keep an Effect error's_tag. See Scoped atoms and SvelteKit.
The docs site has a live example on most pages, and the code under each one is the file that runs.
npm install effect-atom-svelte "effect@~4.0.0"Turn on Svelte's async mode, which the async hooks and server rendering need. In SvelteKit 3, pass it to sveltekit() in vite.config.ts:
// vite.config.ts
sveltekit({
compilerOptions: { experimental: { async: true } },
});Then put a registry around your app, in the root layout:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { RegistryProvider } from "effect-atom-svelte";
const { children } = $props();
</script>
<RegistryProvider>{@render children()}</RegistryProvider>On the server, the provider creates a registry for each request, so visitors never see each other's state. Without one, the server throws No AtomRegistry in context. Installation covers the registry options and apps without SvelteKit.
Each recipe is a complete component. The docs page linked under it explains the details.
<script module lang="ts">
import { Atom } from "effect/reactivity";
const countAtom = Atom.make(0);
const doubledAtom = Atom.make((get) => get(countAtom) * 2);
</script>
<script lang="ts">
import { useAtom, useAtomSet, useAtomValue } from "effect-atom-svelte";
const count = useAtom(countAtom);
const doubled = useAtomValue(doubledAtom);
const setCount = useAtomSet(countAtom);
</script>
<input type="number" bind:value={count.current} />
<button onclick={() => setCount((n) => n + 1)}>Add one</button>
<p>{count.current} × 2 = {doubled.current}</p>Define atoms at module level, in <script module> or a .ts file, not in a component's script. Don't destructure current: const { current } = useAtomValue(atom) reads once and never updates. See Reading and writing and Derived atoms.
<script module lang="ts">
import { Effect } from "effect";
import { Atom } from "effect/reactivity";
const greetingAtom = Atom.make(
Effect.succeed("Hello from an Effect").pipe(Effect.delay("1 second"))
);
</script>
<script lang="ts">
import { useAtomRefresh, useAtomSuspense } from "effect-atom-svelte";
const greeting = useAtomSuspense(greetingAtom);
const refresh = useAtomRefresh(greetingAtom);
</script>
<svelte:boundary>
<p>{await greeting.current}</p>
<button onclick={refresh}>Load again</button>
{#snippet pending()}<p>Loading…</p>{/snippet}
{#snippet failed(error, reset)}
<p>Something went wrong: {String(error)}</p>
<button onclick={reset}>Try again</button>
{/snippet}
</svelte:boundary>greeting.current is a promise of the value. It rejects when the effect fails, which shows the failed snippet. See Async atoms and Suspense.
Give an atom a key and a schema with Atom.serializable, and await it with useAtomResult at the top of a page's script:
// src/routes/todos/todos.ts
import { Effect, Schema } from "effect";
import { AsyncResult, Atom } from "effect/reactivity";
const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String });
type Todo = typeof Todo.Type;
// Stands in for a real request, such as an AtomRpc or AtomHttpApi query.
const fetchTodos: Effect.Effect<ReadonlyArray<Todo>> = Effect.succeed([
{ id: 1, title: "Read the docs" },
]).pipe(Effect.delay("200 millis"));
export const todosAtom = Atom.make(fetchTodos).pipe(
Atom.serializable({
key: "app/todos",
schema: AsyncResult.Schema({ success: Schema.Array(Todo) }),
})
);<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
import { useAtomResult } from "effect-atom-svelte";
import { todosAtom } from "./todos.ts";
const todos = await useAtomResult(todosAtom);
</script>
{#if todos.current._tag === "Success"}
<ul>
{#each todos.current.value as todo (todo.id)}
<li>{todo.title}</li>
{/each}
</ul>
{:else if todos.current._tag === "Failure"}
<p>Could not load the todos.</p>
{/if}The server waits for the first result, renders it and sends it with the page. The browser starts from that result instead of running the effect again. Call these hooks before the script's first await, or the browser misses the server's result. The results are plain text in the page's HTML, so never serialize data the visitor mustn't see. See Server rendering and Hydration.
Every export, with a link to its entry in the API reference. Every hook that takes an atom or a ref also takes a getter, such as () => todoAtom(id), and follows whichever atom it returns. The exception is useAtomRefProp, which takes the ref itself.
| Export | What it does |
|---|---|
RegistryProvider |
Puts a registry in context for its children: one per request on the server, one per mount in the browser. |
HydrationBoundary |
Hydrates state from Hydration.dehydrate, for example returned by a load function or a remote function. |
| Export | What it does |
|---|---|
useAtomValue |
Reads an atom through current, optionally through a transform. |
useAtom |
Reads and writes a writable atom through current, so bind:value works. |
useAtomSet |
Returns a setter that takes a value or an updater. The "promise" and "promiseExit" modes wait for the result. |
useAtomSuspense |
Exposes an async atom as a promise to await in markup, inside a <svelte:boundary>. |
useAtomResult |
Awaited in the script: waits for the first result, then returns a live AsyncResult. |
useAtomRefresh |
Returns a function that runs the atom again. |
useAtomMount |
Keeps an atom alive while the component lives, even when nothing reads it. |
useAtomSubscribe |
Calls a function on every change while the component lives. |
useAtomInitialValues |
Sets starting values for atoms from a component, such as from a prop. |
useAtomRef |
Reads an AtomRef through current. |
useAtomRefProp |
Returns the AtomRef for one property of a ref, to read or set it. |
useAtomRefPropValue |
Reads one property of an AtomRef through current. |
Types: AtomInput, AtomValue, AtomState, WriteMode, WriteOptions, SuspenseOptions, ResultOptions.
| Export | What it does |
|---|---|
provideRegistry |
RegistryProvider from a component's script. Returns the registry. |
getRegistry |
Returns the nearest registry, to read or write atoms outside a hook. Call it while the component initializes. |
Types: RegistryOptions, ProvideRegistryOptions, ProvideRegistryCommon, ProvideNewRegistry, ProvideExistingRegistry.
| Export | What it does |
|---|---|
ScopedAtom.make |
Creates an atom per component subtree: a parent calls provide, and descendants call use. |
Types: ScopedAtom.ScopedAtom, ScopedAtom.MakeOptions, ScopedAtom.TypeId.
| Export | What it does |
|---|---|
handleClientError |
A client handleError hook that keeps the message and _tag of errors your code throws. |
handleServerError |
A server handleError hook that keeps the _tag, but not the message, which could expose details of the server. |
Types: CaughtError, EffectErrorBody.
Atom, AsyncResult, AtomRegistry, AtomRef, AtomRpc, AtomHttpApi and Hydration, so an app can import everything from one place. They are the same modules as in effect/reactivity.
| Package | Supported | Notes |
|---|---|---|
effect |
~4.0.0 |
Pinned to 4.0.x, not ^4.0.0: the bindings use parts of the atom registry that aren't public API, which a minor release of effect can change. Load one copy of effect in your app: see Two copies of effect. |
svelte |
^5.57.2 |
experimental.async is needed for useAtomSuspense, useAtomResult and server rendering. The other hooks work without it. |
@sveltejs/kit |
^3.0.0 or ^2.0.0, optional |
The docs site and its tests run on SvelteKit 3. On SvelteKit 2, the effect-atom-svelte/sveltekit error hooks work with less detail: see Errors in boundaries. |
SvelteKit is optional: see Plain Svelte. The test suites run in Chromium, Firefox and WebKit.
Your atoms, families, runtimes, AtomRpc and AtomHttpApi clients carry over unchanged, as they come from effect/reactivity. What changes is how a component reads them, because a Svelte component's script runs once rather than on every render:
@effect/atom-react |
effect-atom-svelte |
|---|---|
const [count, setCount] = useAtom(countAtom) |
const count = useAtom(countAtom), then read and assign count.current |
useAtomValue(todoAtom(id)) follows id on each render |
Pass a getter: useAtomValue(() => todoAtom(id)) |
useAtomSuspense suspends, inside <Suspense> |
useAtomSuspense returns a promise you await in markup, inside <svelte:boundary> |
| No provider: a module-level default registry, on the server too | No provider: a shared default registry in the browser, and an error on the server |
| Some queries are fetched again straight after hydration | Nothing runs again after hydration unless you set revalidateOnHydrate |
Migrating from atom-react maps every hook, the registry and server rendering.
- 0.x. A minor release can change the API. Every change is in the changelog.
- A community project by Jarred Norris. It is not part of Effect, and the Effect team neither makes nor endorses it.
- Written with AI. Most of the code and docs were written with the help of AI (Claude Opus 5.5). Its behavior is covered by tests in Chromium, Firefox and WebKit.
effect-atom-svelte was inspired by Thomas Foster's Svelte Atoms pull request to effect-smol. The design of the docs' live examples is inspired by Kit Langton's Visual Effect.
Bug reports and suggestions are welcome in GitHub issues. This is a Bun and Turborepo monorepo: the library is in packages/effect-atom-svelte, and the docs site, which is also the demo app, is in apps/demo.
bun install
bun run check # type-checks every package, building the library first
bun run test # Vitest and Playwright, in Chromium, Firefox and WebKit
bun run lintCONTRIBUTING.md covers the repository layout, running the docs site locally, the tests, changesets and releases.