Skip to content

Changelog

[6.0.10] 2026-08-17 Security (1) · Added (1)

Security

  • serializeSnapshot now 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 </script escape alone did not close. <!-- is now escaped to <\!-- alongside the existing </script and 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 no document present. Silent in the browser and in NODE_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), useWatch previously used the useState + 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 on useSyncExternalStore.
  • 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 a useEffect / useCallback deps array would loop or re-fire. Setter now stabilized with useCallback.
  • use(key) returned the closure variable, not state. The hook read atom.get(key) once at the top of the function and returned that local — the useState was only used as a re-render trigger; its actual state was never read. Subtle tear window. Now reads via useSyncExternalStore directly.
  • AtomProvider left a console.log(currentAtoms) in production. Removed.
  • AtomProvider updated atoms in useEffect after first paint. Children rendered with the OLD atom value, then re-rendered with the new value — visible flicker on SSR / hydration. The new <AtomStoreProvider> applies initialValues synchronously via silentUpdate in a useState initializer; children see the hydrated value on first paint.
  • Provider action passed Partial<Value> as full Value. The action stored props.value as Value directly; consumers passing a partial got a destructive object replacement. Documented; consumers needing partial updates should use merge.
  • clone action overwrote the original atom. The previous React-side clone() spread the original data (including key) and re-registered into the global atoms map — stomping the original. Now routes through the base atom’s clone({ register: false }) which uses the deterministic counter suffix.
  • LoadingAtom type was mis-parameterized. The exported type ReactAtom<LoadingAtomActions> & LoadingAtomActions passed LoadingAtomActions as the Value generic, making atom.value typed as the actions bag. Fixed to ReactAtom<boolean, LoadingAtomActions>.

Added

  • AtomStore integration. <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 active AtomStore | 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 of useAtom. Will be removed in v7.
    • Auto-destruction on provider unmount for stores created internally; externally-supplied store prop is caller-owned.
  • useSyncExternalStore everywhere. Per-atom hooks (useState, useValue, use, useWatch) are tear-free under concurrent rendering. The setter returned by useState() is stable across renders.
  • AbortSignal and stable references. Hooks pass the latest closure via refs; clones use deterministic keys.
  • SSR helpers in the main barrel (no /ssr subpath):
    • serializeSnapshot(snapshot, options?) — JSON-stringifies a snapshot with XSS protection: </script> is escaped, U+2028/U+2029 are escaped to their JS forms.
    • serializeStore(store, options?) — convenience over serializeSnapshot(store.snapshot()).
    • <HydrateAtomsScript snapshot={...} id? nonce? /> — emits a typed <script type="application/json"> carrying the payload.
    • readHydration(id?) — client-side picker. Returns null on 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 of AtomStoreContext.
    • AtomProvider: deprecated shim over <AtomStoreProvider>, maps registerinitialAtoms and defaultValueinitialValues.
  • 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 old AtomProvider. Now an overloaded function: useAtom(template) resolves to the store-scoped clone (recommended path); useAtom(key) is the legacy escape hatch — returns undefined when 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. Now react >= 18 to enable useSyncExternalStore.
  • peerDependencies: { "react": ">=18.0.0" } updated accordingly.

Removed

  • AtomContext as a typed key→atom record. Now an alias of AtomStoreContext (whose value is an AtomStore | null).
  • The legacy useAtom(key) from context.tsx. Folded into the overloaded useAtom in store.tsx.

Tests

44 tests across react-atom, helpers, store, ssr (synchronous + streaming), ssr-helpers