@mongez/atom
A framework-agnostic, action-shaped state primitive with first-class SSR isolation, derived values, persistence, and Redux DevTools.
@mongez/atom is the core of the Mongez state-management family. The React adapter lives in @mongez/react-atom; the client-side server-state cache in @mongez/atomic-query.
The atom isn't just a value — it's a value with methods bound to it. Instead of writing setters and helper functions everywhere, you define actions on the atom itself and call them as verbs: cart.push(item), modal.open(), auth.login(creds).
Install
yarn add @mongez/atom
A 30-second tour
import { createAtom, atomCollection, derive } from "@mongez/atom";
const sidebar = createAtom({
key: "ui.sidebar",
default: false,
actions: {
open() { this.update(true); },
close() { this.update(false); },
toggle() { this.update(!this.value); },
},
});
sidebar.toggle();
type Todo = { id: number; text: string; done: boolean };
const todos = atomCollection<Todo>({ key: "todos", default: [] });
todos.push({ id: 1, text: "Buy bread", done: false });
todos.remove(t => t.done);
const incompleteCount = derive("todos.incomplete", get =>
get(todos).filter(t => !t.done).length,
);
incompleteCount.value;
todos.push({ id: 2, text: "Read book", done: false });
incompleteCount.value;
What's in the box
createAtom | The atom factory. |
atomCollection | Array-shaped atoms with mutation verbs. |
derive | Computed atoms with auto-tracked dependencies. |
AtomStore / createAtomStore | Per-request isolation primitive for SSR. |
enableAtomDevtools | Redux DevTools bridge with time-travel. |
persist (option on AtomOptions) | Persist atom values via localStorage or any adapter. |
getAtom, atomsList, atomsObject | Registry helpers. |
Methods on every atom
const counter = createAtom({ key: "counter", default: 0 });
counter.value;
counter.defaultValue;
counter.update(5);
counter.update(prev => prev + 1);
counter.silentUpdate(0);
counter.reset();
counter.silentReset();
counter.onChange((next, prev) => …);
counter.onReset(…);
counter.onDestroy(…);
counter.clone();
counter.destroy();
For object-valued atoms, you also get:
const user = createAtom({
key: "user",
default: { name: "Anon", age: 0 },
});
user.merge({ age: 31 });
user.change("name", "Alice");
user.silentChange("age", 32);
user.get("name");
user.watch("name", (next, prev) => …);
Calling change / merge / watch on a primitive atom (Atom<boolean>, Atom<number>, Atom<string>) is a compile error — the methods are stripped from the type because they would silently corrupt the value at runtime.
Derived atoms
import { createAtom, derive } from "@mongez/atom";
const firstName = createAtom({ key: "first", default: "Ada" });
const lastName = createAtom({ key: "last", default: "Lovelace" });
const fullName = derive("fullName", get => `${get(firstName)} ${get(lastName)}`);
fullName.value;
firstName.update("Grace");
fullName.value;
Conditional reads work — branches re-track dependencies on each run:
const branch = createAtom({ key: "branch", default: "first" as "first" | "last" });
const selected = derive("selected", get =>
get(branch) === "first" ? get(firstName) : get(lastName),
);
Chained derivations propagate: a derive that reads another derive recomputes when either changes.
Persistence
import { createAtom, type PersistAdapter } from "@mongez/atom";
const themeAtom = createAtom({
key: "theme",
default: "light",
persist: true,
});
const myAdapter: PersistAdapter = {
get: (key) => myCache.get(key),
set: (key, value) => myCache.set(key, value),
remove: (key) => myCache.delete(key),
};
const userAtom = createAtom({
key: "user",
default: { name: "Anon" },
persist: myAdapter,
});
Behavior:
- On atom creation the adapter is read. A stored value replaces the default via
silentUpdate (no update event fires for the hydration).
- Every subsequent
update writes through to the adapter.
reset() removes the entry.
For SSR, pair with a cookie-aware adapter so the server can read the persisted value during render — localStorage doesn't exist on the server and the built-in adapter no-ops there.
SSR isolation — AtomStore
The module-level atoms registry is shared per Node process. Two concurrent SSR requests would write to the same atoms. AtomStore solves it:
import { createAtomStore } from "@mongez/atom";
import { userAtom } from "./state";
const store = createAtomStore();
store.use(userAtom).update({ name: "Alice" });
userAtom.value;
store.get("user")?.value;
API:
class AtomStore {
use<V, A>(template: Atom<V, A>): Atom<V, A>;
get<V>(key: string): Atom<V> | undefined;
has(key: string): boolean;
list(): Atom<any>[];
hydrate(snapshot: Record<string, unknown>): void;
snapshot(): Record<string, unknown>;
destroy(): void;
}
For the React side (<AtomStoreProvider>, useAtom, useAtomStore, hydration helpers), see @mongez/react-atom.
DevTools
import { enableAtomDevtools } from "@mongez/atom";
if (process.env.NODE_ENV !== "production") {
enableAtomDevtools({
name: "MyApp",
ignore: [/^mouse\./, /^scroll\./],
});
}
Connects to window.__REDUX_DEVTOOLS_EXTENSION__. You get a live atom list, an update timeline, and time-travel via JUMP_TO_STATE. No-op when the extension isn't installed. Tree-shaken when you don't import it.
Lifecycle events
Each atom emits on the @mongez/events bus under the namespace atoms.${key}:
atoms.${key}.update — fired by update / change / merge
atoms.${key}.reset — fired by reset / silentReset
atoms.${key}.delete — fired by destroy
Namespace matching in @mongez/events is segment-aware, so destroying users.1 does not also destroy users.10.
Examples
A cart with computed totals via getters
type Item = { id: string; price: number; qty: number };
const cart = atomCollection<Item>({
key: "cart",
actions: {
get total() {
return this.value.reduce((sum, i) => sum + i.price * i.qty, 0);
},
setQty(this: Atom<Item[]>, id: string, qty: number) {
this.update(this.value.map(i => (i.id === id ? { ...i, qty } : i)));
},
},
});
cart.push({ id: "a", price: 10, qty: 2 });
cart.total;
cart.setQty("a", 5);
cart.total;
Derived "can checkout" across atoms
const canCheckout = derive("canCheckout", get =>
get(cart).length > 0 &&
get(userAtom).loggedIn &&
!get(checkoutLoading).isLoading,
);
if (canCheckout.value) { }
Persisted preferences
const themeAtom = createAtom({
key: "ui.theme",
default: "light",
persist: true,
});
TypeScript
Atom<V> is a conditional type. Object-only methods (merge, change, silentChange, get(key), watch(key, cb)) only exist when V is an object/array. Atom<boolean>.change(...) is a compile error.
AtomActions<V> no longer collapses to any. Per-action type safety is preserved.
AtomOptions.default: V must be a complete value (no Partial<V> default).
See MIGRATION.md for the v1 → v2 transition.
Related packages
License
MIT