Shared store
Islands are independent React roots, so React Context carries nothing between
them. State two islands share lives outside React, in one module, and the
framework ships that module: @pagedeck/islands/store.
Everything a site needs to use it comes from the same place, including Jotai's
own atom and hooks. Do not add jotai to the site's own dependencies — a
second copy of the library is a second atom factory, and atoms are compared by
object identity.
Declaring atoms
// site/atoms.ts
import { atom } from "@pagedeck/islands/store";
export const cartCount = atom(0);
cartCount.debugLabel = "cartCount";The debugLabel is optional and worth writing: it is how an atom is named if
the framework ever reports something about it.
Delivering the store to every island root
The store reaches components through the root provider stack
(build.rootProviders), which the build wraps the page render in and the
runtime replays around every island root:
// site/providers.ts
import { Provider, store } from "@pagedeck/islands/store";
export default [{ component: Provider, props: { store } }];// pagedeck.config.ts
import providers from "./site/providers.js";
export default defineConfig({
build: {
rootProviders: { stack: providers, module: "./site/providers.js" },
},
});Both halves, always. stack is what the build renders with and module is what
the generated island entry imports in the browser; declaring one without the
other is refused at config load.
Reading and writing
"use client";
import { useAtom } from "@pagedeck/islands/store";
import { cartCount } from "../atoms.js";
export default function CartBadge() {
const [count, setCount] = useAtom(cartCount);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}Any island on the page reading cartCount sees that write, whichever chunk it
shipped in and whenever it hydrated. So does plain JavaScript with no React
involved: store.set(cartCount, 3).
Hydrating from the page
Server values go into the store once, from the page, before any root mounts:
import { hydrateStore } from "@pagedeck/islands/store";
import { cartCount } from "./atoms.js";
hydrateStore([[cartCount, initialCount]]);Not from a hook inside a component. An atom hydrates once per store, and island
hydration order is not controlled under the visible and idle strategies — so
a value handed over by the second island to hydrate would be dropped, and which
island that is depends on where the reader scrolled. Hydrating from the page
takes the question away. A second hydration of an atom keeps the first value and
reports the drop in the browser console.
Callbacks and other escape hatches
Jotai's useAtomCallback reads the default store unless it is passed
{ store }, and says nothing when it does. On a page built this way that
default store is one nothing else uses, so a callback that fell through to it
would read stale values and write into nowhere. Use the framework's hook, which
threads the store for you:
import { useStoreCallback } from "@pagedeck/islands/store";
function AddToCart({ sku }: { sku: string }) {
const addToCart = useStoreCallback((get, set) => {
set(cartCount, get(cartCount) + 1);
});
return <button onClick={() => addToCart()}>Add {sku}</button>;
}Nothing else from jotai/utils is re-exported, and that is deliberate rather
than an oversight. useHydrateAtoms is the one this framework replaces —
hydrateStore above exists because a hook cannot hydrate before roots mount.
The rest of that module builds atoms rather than reaching stores
(selectAtom, atomFamily, atomWithStorage), so nothing about them would
change here, and re-exporting a surface no site has asked for yet would be
guessing at which half of it matters. If you need one, say which and why: the
re-export block in packages/islands/src/store.ts is where it goes, and the
door stays shut rather than being left half open.
What happens if there are two stores
Two stores share nothing — every component still renders, and state simply stops
crossing islands. Nothing in Jotai reports this: its own duplicate-instance
warning lives inside getDefaultStore(), which this design never calls.
Three things stand between a site and that page:
- The build asserts on the emitted chunk graph that no module is in two chunks, and fails naming the module. This is the real guarantee, and it runs where the fault can still be fixed.
- The store module stamps
globalThis, so a second copy of the module adopts the first copy's store instead of minting one. A module evaluated twice therefore yields one store, which is why a linked monorepo package underpagedeck dev— which Vite legitimately evaluates twice — costs nothing. - What is left — a store the framework did not mint, delivered by the
Providerabove — is checked wherever the stack is applied.pagedeck build,pagedeck devand a preview app all refuse it; only a shipped page reports it and carries on, because a canary should not take a visitor's page away.
A store you create yourself and deliver through a provider of your own is not
this check's business, and builds. The check reads one prop of one component:
the store prop of the Provider this package exports.
Version
Jotai is pinned to an exact version rather than a range, because the multi-root behaviour this design rests on is not something Jotai documents. Upgrading it is a decision to re-verify, not a patch bump.