Skip to content

Sync Reference

@zhuangtai-js/sync provides cross-context synchronization for atom creators made with createAtom().

Terminal window
pnpm add @zhuangtai-js/core @zhuangtai-js/sync

Install @zhuangtai-js/core alongside it, because it is a peer dependency of @zhuangtai-js/sync.

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.

Pass sync.key to sync state across same-origin contexts through BroadcastChannel.

const theme = atom("light", {
sync: {
key: "theme",
},
});
theme.set("dark");

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.

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),
},
},
});
  • Omitting sync options 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.is no-op updates are not broadcast.
  • Because received broadcasts write straight to the underlying state, they bypass the set logic of any other plugin wrapped above sync. Prefer createAtom().use(persist).use(sync).
  • SSR or runtimes without BroadcastChannel silently degrade to a plain atom.
  • The default BroadcastChannel is 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 passed channel is managed by the caller.
  • BroadcastChannel only works across same-origin contexts. It does not cross devices and does not persist. Combine it with @zhuangtai-js/persist when 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.