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, and a drop-in setContext or <Provide> reaches a whole subtree without prop drilling.
On this page
Each island is its own hydration root, so a value passed between islands crosses a serialization boundary instead of being shared by reference. For state that lives inside one island, a plain $state is enough, and for app-wide state, a module-level $state already shares across islands with no setup. The tools on this page are for the remaining case, where an instance is born on the server and has to reach other islands. There are two of them: sharing state hands one live object to specific islands as a prop, and context provides a value to a whole subtree so that any island below can read it.
Sharing state
A plain object copies correctly across a serialization boundary, but a live class instance, such as a store or 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 and decode says how to rebuild it. You write both, so nothing is hidden. import.meta.og.wire is a compile-time construct with no import, and the macro generates the codec key. The wire macro page has the full contract, the strict rules and session continuity (id and merge).
Create one instance and pass 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} />The two islands are separate bundles, but they share one live object: when you click Add, the count island repaints. The $state inside the instance is reactive across every island that holds it, without a store library or hand-written events.
The two widgets below are separate islands that share one Cart, passed as a prop:
How it stays live
What keeps the object live is identity, not the codec. Each instance gets one wire id, and the browser rebuilds it once and memoizes it by that id, so every copy of the prop resolves to the same object. An island that hydrates late (visible, idle) joins the current instance when it wakes.
Server-safe by construction
The browser remembers decoded instances, but the server never does. Every request, and every deferred-island render, decodes fresh, so one visitor’s state can never render into another visitor’s HTML. Create the instance per request, in your page or in load, the same as any other prop. The encode snapshot rides inside the props, so the server HTML and the first client paint agree and nothing flickers.
Context
When one object goes to two known islands, a prop is the right tool. Sometimes, though, a value has to reach many islands spread through a subtree, such as a cart shared by a header badge and a button in the page body, a theme or the current user, and threading it through every prop is noise. With context, you provide the value once and each island below reads it with ordinary getContext.
Svelte’s own getContext and setContext work down a component tree, and islands break that tree, so a setContext above an island never reaches inside it on the client. ogygia bridges the same getContext('key') across the split, using the one thing that does continue across it, the DOM. You can provide values in two ways, with a drop-in setContext or with <Provide>. What can cross, how stores stay live across islands and how to pass functions make up the boundary’s contract, which The Boundary describes.
Drop-in setContext
For a layout you already have, the smallest change is to 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 it also records each string-keyed value, so every island can seed its own getContext('user') from it. The result is one flat page root that every island on the page inherits. Symbol keys and function values stay Svelte-only, because they cannot be serialized and so cannot cross. For a value scoped to only part of the tree, use <Provide>, which shadows the page root on the same key.
Define a typed context
createContext(key) is optional TYPED sugar over a string key. You call it to make a provider entry, and .get() reads the typed value. The key is the same string that 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. You can pass a typed handle called with its value (cartCtx(cart) → { cart }), a plain object, or an array of them, merged left to right with 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(), because <Provide> is the provider and has to wrap a subtree. To change the value, mutate the live object you provided, and every reader sees the change.
Read it
An island under the provider reads the value during setup, with either the typed handle or plain getContext, which return the same value:
<script>
import { getContext } from 'ogygia'; // or from 'svelte' — same function
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 it during the island’s setup, exactly as with Svelte’s getContext, and keep the reference. For a wired value, that reference is the one live instance, shared and reactive across every island that read it.
getContext is re-exported from ogygia only for symmetry; it is Svelte’s own function, unchanged. ogygia seeds each island’s context when it hydrates, so a plain read already sees bridged values. Import it from wherever reads best, for example import { setContext, getContext } from 'ogygia' to keep the pair together.
The two widgets below are separate island bundles, and neither receives a prop. Both call cartCtx.get() and reach the same live Cart, so clicking Add repaints the count island:
How it crosses the boundary
Both kinds of provider do two things at once:
- On the server, islands render nested inside 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. - The provider also writes the values into the DOM as a small
<script>: a<Provide>wraps its subtree, and a drop-insetContextemits one page-level marker. The values are serialized with the same codec as island props. On the client, each island is a separate root that reads the providers standing above the spot where it hydrates, and seeds its owngetContextfrom them.
On a `csr = true` page it is plain Svelte context
When Kit hydrates the whole document, ogygia islands degrade to plain components in one tree, so there is no split to bridge. There, setContext, createContext and <Provide> behave exactly like Svelte’s native context, with same-tree reads, no DOM bridge and no error. It is safe to keep importing the drop-in from ogygia in mixed apps: a csr = true route pays nothing for it and it works as expected, because the bridge only engages where islands split the tree.
Live objects vs plain values
Sharing and context both accept any serializable value, whether a string, a number, a plain object or an array. The difference lies in what the reader gets back:
- A plain value comes back as a snapshot, a copy taken when you provided it. Each island gets its own copy, so a change in one is not seen by the other. This suits a theme, a user name or configuration.
- A wired class comes back as one live instance, because the wire id resolves every decode to the same shared, reactive object. Use this when islands have to react to each other’s changes.
So value={'dark'} gives each island the string 'dark', while value={new Cart()} gives every island the same live cart.
Continuity: surviving navigation
By default, state has page lifetime: a navigation creates a fresh instance, and the visitor’s cart is gone. If you give a codec a name, it gets session lifetime, and 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, but because the codec is named, the client joins it with the cart the visitor already had, runs
merge, and gives every island the same live instance, so identity never resets. - By default the live object wins. Without a
merge, the visitor’s in-progress cart takes precedence over the fresh server read, so writemergeto pull the server’s truth in. - It is scoped to the tab, not the server. The name lives in memory in the browser tab (the “Keep”), so another visitor never sees it, the server never remembers it, and a reload starts fresh. A cart that has to survive reloads or follow a login needs real storage, meaning a database plus a
load, andmergeis where the two meet.
A context whose value is a named codec is continuous automatically, with no change to the context needed.
Persistent islands
An island can keep its whole live self across navigation, not only an object:
import Player from '$lib/Player.svelte' with { wake: 'load', keep: 'player' };When the next page uses the same keep name, the live node and its mounted app move into the new slot instead of remounting, so a music player keeps playing through navigation and keeps its scroll position. 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 are kept, not the props.
keep needs the SPA router, because a full page navigation replaces the whole DOM and leaves nothing to move. In dev, a keep island on a page without the router warns.
Half-filled forms
This is on by default and needs no setup: an island form you partly filled in survives navigating away and back within the tab session, and is restored with bind: synced again. Turn it off for one field with data-ogygia-no-keep (useful for a one-time code or a fresh message box), or for everything with ogygia({ router: { forms: false } }).
Continuity ids must be unique per class. If two different classes are both named
'cart', they overwrite each other in the session Keep, and dev warns.
Constraints
- A wire codec needs a top-level
export classin your app source, because the build tags it by module path plus name, so default exports and class expressions don’t work. A context needs an explicit stringkey, eithercreateContext('key')or the string you pass tosetContextor<Provide>, shared by the provider and every reader. - A receiving island has to import a wire class as a value (not
import type) only if it constructs one. To only receive it, as a prop or through context, nothing extra is needed, because the build registers the codec automatically. - A default
decodethat callsnew Cls()needs a constructor with no arguments. Otherwise, writedecodeyourself (as above). - Read context during island setup, like
getContext. A later call, for example in an event handler, has no hydrating island 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). Without a provider above, the reader gets the default orundefined. - Context values must be serializable, like island props: plain data, a
[ogygia.wire]live value or a region snippet. Functions and live stores are dropped rather than crashing the page. - A server island (
render: 'deferred') renders in isolation on the endpoint, so it sees only a context’s default and never the page’s provider. If it needs the live context, put the reader in a nestedwakeisland, which hydrates on the client and joins the provider. import.meta.og.wireis a compile-time construct. The macro generates the reserved codec key at build time, so there is nothing to import and no runtime symbol to manage.
See Constraints for the full boundary rules.