Changelog
[7.0.0] 2026-09-30 Breaking changes (4) · Fixed (4) · Added (5) · Types (1) · Notes (2)
SSR/CSR correctness fixes. @mongez/atom itself stays backward compatible (new options default to the old behavior); the major bump keeps it in lockstep with @mongez/react-atom 7, which opts into the new options and carries the breaking changes.
Breaking changes
@mongez/react-atom7 requires@mongez/atom^7.0.0. It relies onhydratePersisted(),store.serverSnapshot(),clone({ seed })andregister: false, which only exist from this version.- Server rendering without a store now throws (in
@mongez/react-atom). The old singleton fallback, which only warned once and was silent whenNODE_ENV=production, is gone: on a server, hooks throw unless an<AtomStoreProvider>is mounted. persistHydrationdefaults to"deferred"when atoms are created through@mongez/react-atom. In this package the default is unchanged ("immediate").<AtomStoreProvider>destroys its auto-created store one tick after unmount (in@mongez/react-atom), not synchronously.
Fixed
AtomStore.use()seeded new clones from the template’s CURRENT value. One straytemplate.update()on the server therefore leaked into every later request’s fresh store. Store clones now start from the template’s default value (or the queued hydration value).atom.clone()itself still copies the current value unless you pass{ seed: "default" }.store.use(atom)was not idempotent. Passing an atom that is already a scoped clone (for exampleuseAtom(t).useValue()in React, where the hook receives the clone asthis) made a clone of the clone under a different key. It now returns the store’s clone of the same template. Clones of another store map to this store’s clone, and local atoms are returned unchanged.- Key watchers ignored atoms declared with
default: null. Object-ness was decided once from the default, sowatch("name", …)never fired after such an atom received an object. It is now decided per update (old or new value is an object). destroy()on a template unsubscribed every clone of it. It ranunsubscribeNamespace("atoms.<key>"), which also matchesatoms.<key>.clone.<n>.*. It now removes only the atom’s ownupdate,resetanddeletetopics. It also removes the registry entry only when that entry is the atom being destroyed, so destroying aregister: falseatom no longer evicts a registered atom with the same key.
Added
clone({ seed: "current" | "default" })(CloneAtomOptions)."default"copies the default value instead of the current one.register: falseonAtomOptions(and onatomCollection). Creates a “local” atom outside the globalatomsregistry.isLocalAtom(atom)andgetAtomOrigin(clone)expose the bookkeeping. Give each local atom a unique key: events still travel on the sharedatoms.<key>.*bus.persistHydration: "immediate" | "deferred"andatom.hydratePersisted()."deferred"skips the storage read at creation;hydratePersisted()reads the adapter once and applies the value withupdate()so subscribers re-render. An update or reset before it wins. Default stays"immediate".store.serverSnapshot(scoped). The value a scoped atom renders on the first pass of a server-rendered tree, frozen at the first read;hydrate()re-seeds it anddestroy()drops it.@mongez/react-atomuses it as React’sgetServerSnapshot."sideEffects": falseinpackage.jsonso bundlers can tree-shake unused exports.
Types
atomCollectiontyping conflict fixed (type-only, no runtime change).AtomCollectionActionsextended theAtomActionsindex signature (every member a function) while declaringlength: number, whichtscrejects (TS2411 on the interface, TS2322 inatomCollection).lengthnow lives on the new exportedAtomCollection<Value>type,Atom<Value[], AtomCollectionActions<Value>> & { readonly length: number }, which is whatatomCollection()returns.todos.lengthis still typednumber; the accessor at runtime is unchanged.AtomActionsis left untouched, so atoms typed with the defaultAtomActionskeep their exact typing.
Notes
- New members on the
Atomtype:hydratePersisted. - New exported type:
AtomCollection.AtomCollectionActionsno longer declareslength(see Types).
[6.0.9] 2026-05-27 Fixed (8) · Added (8) · Changed (5) · Removed (2) · Dependency bumps (1)
Fixed
- Watcher leak / wrong-removal in
atom.watch(key, cb). The unsubscribe captured an index from the subscription site, and that index drifted as earlier callbacks unsubscribed — splicing the wrong (or out-of-range) callback. Unsubscribe is now identity-based: it removes the callback by reference. - Watcher diff used the raw
newValueargument. When callers passed an updater function toupdate(...), the watcher comparison ranget(newValue, key)on the function itself, so every watcher fired (or none did) regardless of what changed. Now uses the resolvedupdatedValue. reset()returned dead code. The function assignedconst update = this.update(...)(returnsvoid) and thenreturn update. The local was alwaysundefined. Removed.reset()didn’t deep-clone the default. After the firstupdatethat mutatedcurrentValue, subsequentreset()calls handed callers a reference into shared default state. Now clones via@mongez/reinforcements’clone.clone()collisions on heavy use. The 4-digitRandom.int(1000, 9999)suffix collided in birthday-paradox territory after a few thousand clones. Replaced with a monotonic counter (.clone.1,.clone.2, …).get(key)auto-bound any function-valued property. Every function has a.bind, so the implementation rebound every function returned fromget()tothis.currentValue. That broke pre-bound methods and generated a new function identity per read. Removed entirely.change/silentChangeon primitive atoms silently corrupted state. Callingatom.change("foo", "bar")on abooleanatom spread the primitive ({...true, foo: "bar"}→{foo: "bar"}) and replaced the boolean with an object. Now a compile error at the type level (see “Changed”).- Getter-based actions crashed
createAtom.Object.keys(actions).forEach(k => actions[k].bind(atom))invoked any getter eagerly; if the getter referencedthis.value(which wasn’t on the atom yet) it returnedundefined, and.bindblew up. The action installer now usesObject.getOwnPropertyDescriptorand forwards getters as getters.
Added
derive(key, get => …)— derived atoms with auto-tracked dependencies. The compute function runs eagerly on creation and on every dependency change. Dynamic dependency graphs are supported (conditional reads add/drop deps across runs). Errors insidecomputeare surfaced asynchronously without breaking the source atom’s update cycle.persist: true | PersistAdapter— atom-level persistence.trueuses the built-in localStorage adapter (no-ops on the server); aPersistAdapterobject lets you plug in cookies, IndexedDB,@mongez/cache, etc. The adapter is called for the initial read (sync or async), every update writes through, andreset()removes the entry. Sync errors and async rejections from the adapter are swallowed — a transient storage error (quota, private-mode block) never crashes the atom.AtomStore+createAtomStore. Per-request isolation primitive for SSR. Each store holds scoped clones of atom templates, exposesuse(template),get(key),hydrate(snapshot),snapshot(), anddestroy(). The React-side glue (AtomStoreProvider,useAtom,useAtomStore) lives in@mongez/react-atom.clone({ register: false })option. Lets the store create scoped clones without polluting the module-levelatomsregistry.enableAtomDevtools(options?). Redux DevTools bridge with initial-snapshot, per-update timeline entries,JUMP_TO_STATE/JUMP_TO_ACTIONtime-travel viasilentUpdate, ignore-patterns, configurable scan interval for late-registered atoms.- AI kit.
llms.txt,llms-full.txt, andskills/folder (README,overview,atoms,collections,stores,actions,devtools,recipes) for tool-assisted development. - Test suite. 52 unit tests across
atom,atom-collection,atom-store, anddevtools. - CI. GitHub Actions workflow: Node 18/20/22 × Ubuntu, plus Node 20 × Windows.
Changed
Atom<V, A>is now a conditional type. Object-only methods (merge,change,silentChange,get(key),watch(key, cb)) are removed from the type whenVis a primitive.Atom<boolean>.change(...)is a compile error.Atom<any>keeps both surfaces (legacy permissive default).AtomActions<V>no longer includes| any. Was[key: string]: (...) => any | any. The| anycollapsed the entire type toanyand defeated per-action type safety. Now just(this: Atom<V>, ...args: any[]) => any.AtomOptions.defaultisV, notV | Partial<V>.Partial<V>accidentally allowed incomplete defaults that the type didn’t reflect at runtime.change/silentChangesignatures are(key: T, newValue: V[T]), not(key, newValue: any). The wideranydefeated type safety for known-shape atoms.clone()accepts{ register?: boolean }. Pure addition for stores; existing callers (atom.clone()) keep working.
Removed
- Auto-
bindinatom.get(key). Functions returned fromget()are no longer rebound tocurrentValue. If your code relied on this, wrap the call:atom.get("fn")?.bind(atom.value). - Random-suffix clone keys (
...Cloned9123). Replaced by deterministic counter (...clone.1,...clone.2).
Dependency bumps
@mongez/reinforcements: ^2.3.10→^3.1.0. Compatible API for the surfaces atom uses (clone,get). See reinforcements v3 MIGRATION for the full diff.
Tests
46 + 6 devtools + 7 derive + 12 persist = 71 passing