Data & state
State & Context
Share one live object across islands. A class with static wire = import.meta.og.wire({…}) crosses island boundaries as a prop; a drop-in setContext or <Provide> reaches a whole subtree without prop drilling.
On this page
When to use
Each island is its own hydration root, so a value handed between islands crosses a serialization boundary, not a reference. This page covers the two ways to share anyway: sharing state hands one live object to specific islands as a prop, and context provides a value to a whole subtree so any island below can read it. For state used inside one island, a plain $state is enough. For app-wide state, a plain module-level $state already shares across islands with no ceremony. The tools here are for when the instance is born on the server and must reach other islands.
Sharing state
A prop crosses a serialization boundary. A plain object copies fine; a live class instance (a store, an orchestrator) does not. A class opts in by declaring how it travels, with the wire macro:
export class Cart {
items = $state<string[]>([]);
get count() { return this.items.length; }
add(item: string) { this.items.push(item); }
static wire = import.meta.og.wire({
encode: (c: Cart) => $state.snapshot(c.items),
decode: (items: string[]) => Object.assign(new Cart(), { items }),
});
}encode says what leaves. decode says how to rebuild. Both are yours, so nothing is hidden. import.meta.og.wire is a compile construct: no import, and the macro mints the codec key. See the wire macro for the full contract, the strict rules, and session continuity (id / merge).
Make one instance and hand it to as many islands as you like:
<script>
import CartCount from './CartCount.svelte' with { wake: 'load' };
import AddButton from './AddButton.svelte' with { wake: 'visible' };
import { Cart } from './cart.svelte.js';
const cart = new Cart();
</script>
<CartCount {cart} />
<AddButton {cart} />They are separate bundles, but they share one live object. Click Add and the count island repaints: $state inside the instance is reactive across every island holding it. No store library, no manual events.
The two widgets below are two separate islands sharing one Cart passed as a prop:
How it stays live
Liveness is identity, not the codec. Each instance mints one wire id; the browser rebuilds it once and memoizes by that id, so every copy of the prop reunites into the same object. A late-hydrating island (visible, idle) joins the current instance when it wakes.
Server-safe by construction
The browser remembers decoded instances; the server never does. Each request, and each deferred-island render, decodes fresh, so one visitor’s state can’t render into another’s HTML. Build the instance per request (in your page or load), exactly like any other prop. The encode snapshot rides inside the props, so the server HTML and the client’s first paint agree: no flicker.
Context
Passing one object to two known islands is a prop. But when a value must reach many islands scattered down a subtree (a cart shared by a header badge and a page-body button, a theme, a current user), threading it through every prop is noise. Provide it once; any island below reads it with plain getContext.
Svelte’s own getContext / setContext work down a component tree. Islands break that tree: a setContext above an island never reaches inside it on the client. Ogygia bridges the same getContext('key') across the split, through the thing that does survive it: the DOM. There are two ways to provide — a drop-in setContext, or <Provide>.
Drop-in setContext
The smallest change, for a layout you already have: swap the import.
// import { setContext } from 'svelte';
import { setContext } from 'ogygia';
setContext('user', currentUser); // now reaches child islands tooIt does exactly what Svelte’s setContext does, and on the server also records each string-keyed value so every island can seed its own getContext('user') from it — one flat page root that every island on the page inherits. Symbol keys and function values stay Svelte-only (they can’t serialize, so they don’t cross). For a value scoped to only part of the tree, reach for <Provide>, which shadows the page root on the same key.
Define a typed context
createContext(key) is optional TYPED sugar over a string key: callable to make a provider entry, .get() to read (typed). The key is the same string plain getContext uses, so typed and untyped code read the same value.
import { createContext } from 'ogygia';
import { Cart } from './cart.js';
export const cartCtx = createContext<Cart>('cart'); // get(): Cart | undefined
export const theme = createContext<'light' | 'dark'>('theme', 'light'); // has a defaultProvide it
<Provide values={…}> wraps a subtree and provides its values to every island inside. Pass a typed handle called with its value (cartCtx(cart) → { cart }), a plain object, or an array of them merged left→right (falsy entries skipped):
<script>
import { Provide } from 'ogygia';
import CartBadge from './CartBadge.svelte' with { wake: 'load' };
import AddButton from './AddButton.svelte' with { wake: 'visible' };
import { Cart, cartCtx } from './cart.svelte.js';
const cart = new Cart();
</script>
<Provide values={[cartCtx(cart)]}>
<CartBadge />
<!-- ...any depth of markup and islands... -->
<AddButton />
</Provide>There is no .set(): <Provide> is the provider, because it has to wrap a subtree. To change the value, mutate the live object you provided; every reader sees it.
Read it
Any island under the provider reads it during setup — either the typed handle or plain getContext, they are the same value:
<script>
import { getContext } from 'svelte';
import { cartCtx } from './cart.svelte.js';
const cart = cartCtx.get(); // typed — the provided Cart, or undefined
// const cart = getContext('cart'); // untyped — same key, same value
</script>
<span>{cart?.count ?? 0}</span>Read during the island’s setup, exactly like Svelte’s getContext. Keep the reference: for a wired value it is the one live instance, shared and reactive across every island that read it.
The two widgets below are separate island bundles. Neither receives a prop: both call cartCtx.get() and reach the same live Cart. Click Add; the count island repaints:
How it crosses the boundary
Both providers do two things at once:
- On the server, islands render nested in the page’s SSR tree, so the provider calls Svelte’s native
setContext: nested islands see the value in the server pass, and the first paint is correct. - It also writes the values into the DOM as a small
<script>(a<Provide>wraps its subtree; a drop-insetContextemits one page-level marker), serialized with the same codec as island props. On the client, each island — a separate root — reads the providers standing above where it hydrates and seeds its owngetContextfrom them.
Live objects vs plain values
Both sharing and context accept any serializable value: a string, a number, a plain object, an array. The difference is what the reader gets back:
- A plain value comes back as a snapshot: a copy taken when you provided it. Two islands each get their own copy, so a change in one is not seen by the other. Perfect for a theme, a user name, config.
- A wired class comes back as one live instance: the wire id reunites every decode into the same object, shared and reactive. Use this when islands must react to each other’s changes.
So value={'dark'} gives each island the string 'dark'; value={new Cart()} gives every island the same live cart.
Continuity: surviving navigation
By default state has page lifetime: a navigation mints a fresh instance and the visitor’s cart is gone. Give a codec a name and it gains session lifetime: the same live object follows the visitor across every page in the tab.
static wire = import.meta.og.wire({
id: 'cart', // a name → session lifetime (one per tab)
encode: (c) => $state.snapshot(c.items),
decode: (d) => new Cart(d),
merge: (live, fresh) => { // both exist after a navigation — reconcile INTO live
live.serverStamp = fresh.serverStamp; // pull server truth (prices, stock, auth)
},
});- On the next page the server still sends a fresh snapshot. Because the codec is named, the client reunites it with the cart the visitor already had, runs
merge, and hands every island the same live instance: identity never resets. - Default is live-wins: with no
merge, the visitor’s in-progress cart beats the server re-read. Overridemergeto pull server truth in. - Tab-scoped, not server-wide. The name lives in memory in the browser tab (the “Keep”). Another visitor never sees it; the server never remembers; a reload starts fresh. For a cart that survives reloads or follows a login, that is real storage: a database plus a
load, andmergeis where the two meet.
A context whose value is a named codec is continuous for free, with no context change needed.
Persistent islands
An island can keep its whole live self across navigation, not just an object:
import Player from '$lib/Player.svelte' with { wake: 'load', keep: 'player' };Same keep name on the next page → the live node and its mounted app relocate onto the new slot instead of remounting. A music player keeps playing through navigation, mid-scroll position and all. The new page’s props are pushed into the live component, so a track that changes per route still updates: only the app (and its $state) is kept, not the props.
keep needs the SPA router: a full-page navigation replaces the whole DOM, so there is nothing to relocate. In dev, a keep island on a router-less page warns.
Half-filled forms
Ambient and on by default: an island form you were filling survives navigation and back within the tab session, restored with bind: resynced. Opt one field out with data-ogygia-no-keep (a one-time code, a fresh-message box), or turn it all off with ogygia({ router: { forms: false } }).
Two continuity IDs must be unique per class: name two different classes
'cart'and they clobber each other in the session Keep (dev warns).
Constraints
- A wire codec needs a top-level
export classin your app source (the build tags it by module path plus name; no default exports or class expressions). A context needs an explicit stringkey—createContext('key'), or the string you passsetContext/<Provide>— that the provider and every reader share. - A receiving island must import a wire class as a value (not
import type) only if it constructs one; to merely receive it (as a prop or via context) nothing extra is needed: the build auto-registers the codec. - A default
decodethat callsnew Cls()needs a zero-argument constructor. Otherwise writedecodeyourself (as above). - Read context during island setup (like
getContext). Called later (in an event handler), there is no island being hydrated to anchor the read, so read once and keep the reference. - A provider must sit above the consuming islands:
<Provide>above them in the DOM, or a drop-insetContextanywhere on the page (the flat page root). No provider above → the default (orundefined). - Context values must be serializable, like island props — plain data, a
[ogygia.wire]live value, a region snippet; never a function or a live store (those are dropped, never crash the page). - A server island (
render: 'deferred') renders in isolation on the endpoint, so it sees only a context’s default, never the page’s provider. If it must read the live context, put the reader in a nestedwakeisland that hydrates on the client and joins the provider. import.meta.og.wireis a compile construct: the macro mints the reserved codec key at build, so there is nothing to import and no runtime symbol to manage.
See Constraints for the full boundary rules.