Changelog
[6.0.10] 2026-08-17 Security (1) · Added (1)
Security
serializeSnapshotnow escapes<!--(src/ssr.tsx). The serializer already escaped</script, which stops the obvious breakout, but<!--inside the payload switches the HTML parser from “script data state” into “script data escaped state” — and in that state the real</script>React emits next is swallowed as data rather than closing the element. Everything after it, up to the next</script>, is then parsed as script. Since a snapshot is by definition server state (records, user input, API values) embedded into the page, that’s an XSS path that the</scriptescape alone did not close.<!--is now escaped to<\!--alongside the existing</scriptand U+2028/U+2029 escapes; the JSON parses identically.
Added
- Development warning when an atom hook falls back to the module-level singleton outside a browser (
src/store.tsx). Without a mounted<AtomStoreProvider>, hooks use the singleton atom — correct for a client-only app (one process, one user), but on an SSR server it means every concurrent request reads and writes the same object, so one user’s state can be rendered into another user’s page. The fallback can’t simply be removed without breaking the documented client-only usage, so it now warns once per process when it is hit with nodocumentpresent. Silent in the browser and inNODE_ENV=production; no behaviour change either way.
[6.0.9] 2026-05-27 Fixed (8) · Added (19) · Changed (4) · Removed (2)
Fixed
- React 18 tearing across every hook.
useState,useValue,use(key),useWatchpreviously used theuseState + useEffect(onChange)pattern. Between the synchronous render snapshot and the effect-time subscription, the atom could change → stale read → siblings could disagree on the same atom’s value during a concurrent render. Every hook is now built onuseSyncExternalStore. useState()returned a fresh setter every render.[value, atom.update.bind(atom)]produced a new function identity on each render. Any consumer putting the setter in auseEffect/useCallbackdeps array would loop or re-fire. Setter now stabilized withuseCallback.use(key)returned the closure variable, not state. The hook readatom.get(key)once at the top of the function and returned that local — theuseStatewas only used as a re-render trigger; its actual state was never read. Subtle tear window. Now reads viauseSyncExternalStoredirectly.AtomProviderleft aconsole.log(currentAtoms)in production. Removed.AtomProviderupdated atoms inuseEffectafter first paint. Children rendered with the OLD atom value, then re-rendered with the new value — visible flicker on SSR / hydration. The new<AtomStoreProvider>appliesinitialValuessynchronously viasilentUpdatein auseStateinitializer; children see the hydrated value on first paint.Provideraction passedPartial<Value>as fullValue. The action storedprops.value as Valuedirectly; consumers passing a partial got a destructive object replacement. Documented; consumers needing partial updates should usemerge.cloneaction overwrote the original atom. The previous React-sideclone()spread the originaldata(includingkey) and re-registered into the globalatomsmap — stomping the original. Now routes through the base atom’sclone({ register: false })which uses the deterministic counter suffix.LoadingAtomtype was mis-parameterized. The exported typeReactAtom<LoadingAtomActions> & LoadingAtomActionspassedLoadingAtomActionsas theValuegeneric, makingatom.valuetyped as the actions bag. Fixed toReactAtom<boolean, LoadingAtomActions>.
Added
AtomStoreintegration.<AtomStoreProvider>scopes atoms per subtree (per SSR request, per test, per multi-tenant boundary). Children of the provider operate on store-scoped clones of every atom they touch; without a provider, hooks fall back to the module-level singleton (correct client-only default).useAtomStore(): returns the activeAtomStore | null.useAtom(template | key): overloaded — pass an atom to resolve to its scoped clone, or pass a key to look up a scoped atom by string.useResolvedAtom: deprecated alias ofuseAtom. Will be removed in v7.- Auto-destruction on provider unmount for stores created internally; externally-supplied
storeprop is caller-owned.
useSyncExternalStoreeverywhere. Per-atom hooks (useState,useValue,use,useWatch) are tear-free under concurrent rendering. The setter returned byuseState()is stable across renders.AbortSignaland stable references. Hooks pass the latest closure via refs; clones use deterministic keys.- SSR helpers in the main barrel (no
/ssrsubpath):serializeSnapshot(snapshot, options?)— JSON-stringifies a snapshot with XSS protection:</script>is escaped,U+2028/U+2029are escaped to their JS forms.serializeStore(store, options?)— convenience overserializeSnapshot(store.snapshot()).<HydrateAtomsScript snapshot={...} id? nonce? />— emits a typed<script type="application/json">carrying the payload.readHydration(id?)— client-side picker. Returnsnullon the server, on missing tag, or on malformed JSON (with a console error for visibility).DEFAULT_HYDRATION_SCRIPT_ID—"__mongez_atom_state".
- Backwards-compatibility shims.
AtomContext: deprecated alias ofAtomStoreContext.AtomProvider: deprecated shim over<AtomStoreProvider>, mapsregister→initialAtomsanddefaultValue→initialValues.
- End-to-end SSR test suite: server snapshot → client hydrate round-trip with no console errors, concurrent-request isolation across stores, lazy hydration of atoms not yet touched server-side, streaming renderer (
renderToPipeableStream) under a real Node environment. - AI kit:
llms.txt,llms-full.txt,skills/(README,overview,atoms,presets,ssr,recipes). - CI. Node 18/20/22 × Ubuntu, Node 20 × Windows, React 18 + React 19.
Changed
useAtom(key)semantics changed. Previously a context-bound clone lookup that only worked under the oldAtomProvider. Now an overloaded function:useAtom(template)resolves to the store-scoped clone (recommended path);useAtom(key)is the legacy escape hatch — returnsundefinedwhen no provider is mounted or the key has not been used yet.- Per-atom hooks return the store-scoped clone’s values when a provider is mounted. Previously they always read from the module-level template. If your app relied on the singleton behavior under what is now an
AtomStoreProvider, you’ll see different values. - React 18 minimum. Was
react > 16.8. Nowreact >= 18to enableuseSyncExternalStore. peerDependencies: { "react": ">=18.0.0" }updated accordingly.
Removed
AtomContextas a typed key→atom record. Now an alias ofAtomStoreContext(whose value is anAtomStore | null).- The legacy
useAtom(key)fromcontext.tsx. Folded into the overloadeduseAtominstore.tsx.
Tests
44 tests across react-atom, helpers, store, ssr (synchronous + streaming), ssr-helpers