Sync Reference
@zhuangtai-js/sync provides cross-context synchronization for atom creators made with createAtom().
Install
Section titled “Install”pnpm add @zhuangtai-js/core @zhuangtai-js/syncInstall @zhuangtai-js/core alongside it, because it is a peer dependency of @zhuangtai-js/sync.
Install the plugin
Section titled “Install the plugin”Install sync on an atom creator.
import { createAtom } from "@zhuangtai-js/core";import { sync } from "@zhuangtai-js/sync";
const atom = createAtom().use(sync);The default atom() export is not extended. Only atoms created with this creator accept sync options.
Sync an atom
Section titled “Sync an atom”Pass sync.key to sync state across same-origin contexts through BroadcastChannel.
const theme = atom("light", { sync: { key: "theme", },});
theme.set("dark");Configure a channel
Section titled “Configure a channel”Synchronization uses a BroadcastChannel named after key by default. Custom channel objects need to implement postMessage and addEventListener("message", ...), matching the methods of the same name on BroadcastChannel.
const channel = new BroadcastChannel("count");
const count = atom(0, { sync: { key: "count", channel, },});If channel is omitted, the plugin uses new BroadcastChannel(key). Under SSR or a runtime without BroadcastChannel, the atom silently degrades to a plain atom with no sync and no error.
Configure a codec
Section titled “Configure a codec”The default codec uses JSON.stringify and JSON.parse, and rejects NaN, ±Infinity, and invalid Date values before encode (JSON would otherwise silently turn them into null). Top-level undefined, functions, and symbols also throw during encode instead of being sent to the channel.
const count = atom(0, { sync: { key: "count", codec: { encode: (value) => String(value), decode: (rawValue) => Number(rawValue), }, },});Semantics
Section titled “Semantics”- Omitting
syncoptions leaves the atom unchanged. - Local updates encode first; only after a successful encode does the value commit locally and get broadcast as the already-encoded payload. If encode fails, memory stays unchanged and nothing is broadcast.
- Incoming broadcasts are decoded and written straight to the underlying state, so they are not re-broadcast and echo loops are avoided.
- Remote decode failures are isolated: local state is unchanged, the error does not escape the message handler, and a diagnostic is written with
console.error. Object.isno-op updates are not broadcast.- Because received broadcasts write straight to the underlying state, they bypass the
setlogic of any other plugin wrapped abovesync. PrefercreateAtom().use(persist).use(sync). - SSR or runtimes without
BroadcastChannelsilently degrade to a plain atom. - The default
BroadcastChannelis unref’ed on runtimes that support it, such as Node, so a synced atom never blocks process exit. Sync keeps working for the lifetime of the process, and an explicitly passedchannelis managed by the caller. BroadcastChannelonly works across same-origin contexts. It does not cross devices and does not persist. Combine it with@zhuangtai-js/persistwhen you need persistence.- Async channels are not supported.
@zhuangtai-js/sync exports these public types:
export type SyncCodec = { readonly encode: (value: unknown) => string; readonly decode: <Value>(rawValue: string, initialValue: Value) => Value;};
export type SyncMessageEvent = { readonly data: string;};
export type SyncChannel = { readonly postMessage: (message: string) => void; readonly addEventListener: (type: "message", listener: (event: SyncMessageEvent) => void) => void;};
export type SyncOptions = { readonly key: string; readonly channel?: SyncChannel; readonly codec?: SyncCodec;};SyncOptions.key is required. channel and codec are optional.