npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mongez/react-atom

v6.0.9

Published

A simple state management tool for React Js.

Readme

@mongez/react-atom

React hooks layer on top of @mongez/atom — every atom carries its own useValue / useState / use(key) / useWatch / <Provider>, plus preset atoms for toggles, loading flags, fetch lifecycles, and modal portals, all SSR-safe.

npm license bundle size downloads


Why @mongez/react-atom?

useState is component-local — sharing a value across siblings means lifting it or wiring a context every time. Zustand and Jotai solve sharing but each invent their own create / atom() shape, their own hook conventions (useStore, useAtom/useAtomValue/useSetAtom), and their own escape hatches for SSR. Recoil is heavier, less actively maintained, and still requires you to write atoms and selectors with selector wrappers.

@mongez/react-atom is the React hooks layer on top of @mongez/atom — the same framework-agnostic atom you already know, just with hooks attached as instance methods. One atom, multiple hooks on it. No useAtomValue / useSetAtom distinction, no selector wrappers, no second mental model. All built on useSyncExternalStore so reads are tear-free under React 18 concurrent rendering, and an <AtomStoreProvider> ships in the box for per-request SSR isolation.

import { atom } from "@mongez/react-atom";

const counterAtom = atom({ key: "counter", default: 0 });

function Counter() {
  const count = counterAtom.useValue();
  return (
    <button onClick={() => counterAtom.update(count + 1)}>
      Count: {count}
    </button>
  );
}

Features

| Feature | Description | |---|---| | Hooks on every atom | useValue, useState, use(key), useWatch, and <Provider> are instance methods on the atom — no useAtomValue / useSetAtom split. | | Granular subscriptions | atom.use("name") re-renders only when name changes; sibling keys in the same atom don't wake the component. | | Tear-free under React 18 | Every hook is built on useSyncExternalStore — concurrent rendering can't show two components disagreeing on the same atom. | | Stable setter identity | useState() returns a setter with the same identity across renders — safe in useEffect / useCallback deps without looping. | | Preset atoms | openAtom, loadingAtom, fetchingAtom, and portalAtom collapse 80% of state work to one line. | | <AtomStoreProvider> | Per-subtree atom isolation — each SSR request gets its own scoped clones. Concurrent requests can't see each other's state. | | Hydration helpers | <HydrateAtomsScript>, readHydration(), and serializeStore ship a server-to-client transport with </script> and U+2028/U+2029 escaped. | | TypeScript-first | Atom value and action types flow into every hook; use("name") is typed to the keys of the atom's value. | | Backward-compatible | Legacy <AtomProvider> and AtomContext survive as deprecated shims while you migrate. | | Zero registry coupling | Module-level singleton mode is the default — providers are opt-in for SSR and isolation. |


Installation

npm install @mongez/react-atom
yarn add @mongez/react-atom
pnpm add @mongez/react-atom

Peer dependencies: react >= 18. The package depends on @mongez/atom directly — installing this package also installs that core layer.

React 18 or newer is mandatory. useSyncExternalStore is required, and there is no plan to backport. If you're on React 16/17, stay on @mongez/react-atom@5.


Quick start

Create an atom, mount its hooks anywhere — no provider, no context wiring.

// atoms/counter.ts
import { atom } from "@mongez/react-atom";

export const counterAtom = atom({
  key: "counter",
  default: 0,
  actions: {
    increment() { this.update(this.value + 1); },
    decrement() { this.update(this.value - 1); },
    reset()     { this.update(0); },
  },
});
// components/Counter.tsx
import { counterAtom } from "../atoms/counter";

export function Counter() {
  const count = counterAtom.useValue();
  return (
    <div>
      <button onClick={() => counterAtom.decrement()}>−</button>
      <span>{count}</span>
      <button onClick={() => counterAtom.increment()}>+</button>
      <button onClick={() => counterAtom.reset()}>reset</button>
    </div>
  );
}

Any other component that calls counterAtom.useValue() subscribes to the same value — no lifting, no context. That's the entire surface for client-only apps. Everything below is depth on the same handful of exports.


Atoms and their hooks

atom(options) is the React-aware version of createAtom from @mongez/atom. The returned atom carries every base method (update, merge, change, onChange, ...) plus a small set of hooks bound to the instance.

atom.useValue()

Subscribes to the atom; returns the current value; re-renders the component whenever the value changes.

const user = userAtom.useValue();
return <h1>Hello {user.name}</h1>;

atom.useState()

Returns [value, setValue] — the same shape as React's useState, but the value lives in the atom and is visible to every other subscriber.

const [count, setCount] = counterAtom.useState();
return <button onClick={() => setCount(count + 1)}>{count}</button>;

The setter accepts an updater function: setCount(prev => prev + 1).

The setter's identity is stable across renders. Safe to list in useEffect / useCallback dependency arrays without re-triggering or looping. This is the React-18 rebuild paying off — pre-v6 code that put the setter in deps used to misbehave.

Prefer useValue() + calling the atom's methods directly. It keeps updates explicit and traceable, especially when paired with custom actions:

const counterAtom = atom({
  key: "counter",
  default: 0,
  actions: { increment() { this.update(this.value + 1); } },
});

function Counter() {
  const count = counterAtom.useValue();
  return <button onClick={() => counterAtom.increment()}>{count}</button>;
}

atom.use(key)

For object-valued atoms: subscribes to one key only. Re-renders only when that key changes — sibling keys in the same atom don't wake the component.

const profileAtom = atom({
  key: "profile",
  default: { name: "Anon", email: "", lastSeen: 0 },
});

function NameDisplay() {
  const name = profileAtom.use("name");
  return <span>{name}</span>;
}

// Updating lastSeen does NOT re-render NameDisplay.
setInterval(() => profileAtom.change("lastSeen", Date.now()), 1000);

use(key) is typed against the atom's value shape — profileAtom.use("nope") is a compile error.

atom.useWatch(key, callback)

Effect-style watcher. Fires the callback when the key changes, but doesn't re-render the host component. Use for analytics, logging, persistence side-effects.

function AnalyticsTracker() {
  userAtom.useWatch("name", (next, prev) => {
    analytics.track("name_changed", { from: prev, to: next });
  });
  return null;
}

<atom.Provider value={…}>

A React component that pushes a value into the atom on mount and on value prop change. Useful for testing (inject a known value into the subtree) and for "set on mount" patterns where state comes from props.

function App({ initialTheme }) {
  return (
    <themeAtom.Provider value={initialTheme}>
      <RestOfApp />
    </themeAtom.Provider>
  );
}

<atom.Provider> updates via useEffect — the value is applied after first paint, not synchronously during render. The first frame still reads the atom's pre-existing value. For SSR-correct pre-fill, use <AtomStoreProvider initialValues={...}> instead.

atomCollection(options)

The React-aware version of atomCollection from @mongez/atom. Inherits the array helpers (push, unshift, pop, shift, remove, replace, ...) AND every hook above.

import { atomCollection } from "@mongez/react-atom";

type Todo = { id: number; text: string; done: boolean };
const todosAtom = atomCollection<Todo>({ key: "todos", default: [] });

todosAtom.push({ id: 1, text: "Buy bread", done: false });

function TodoList() {
  const todos = todosAtom.useValue();
  return <ul>{todos.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

Naming convention: suffix atom variables with Atom (e.g. counterAtom, userAtom) — prevents collisions with locals and makes it obvious at call sites that you're working with a reactive value.


Preset atoms

Four pre-built atom shapes that collapse the most common state patterns to one line. They're regular atoms underneath — every base method and every React hook still works.

openAtom(key, defaultOpened?)

A boolean-shaped toggle with open, close, toggle, and a useOpened() hook.

import { openAtom } from "@mongez/react-atom";

const sidebarAtom = openAtom("sidebar", /* defaultOpened */ false);

function ToggleButton() {
  return <button onClick={() => sidebarAtom.toggle()}>≡</button>;
}

function Sidebar() {
  const opened = sidebarAtom.useOpened();
  return opened ? <Drawer /> : null;
}

loadingAtom(key, defaultLoading?)

A boolean with startLoading / stopLoading / toggleLoading verbs. Use it when you just need a flag — for richer fetch state, reach for fetchingAtom.

import { loadingAtom } from "@mongez/react-atom";

const usersLoadingAtom = loadingAtom("usersLoading");

async function fetchUsers() {
  usersLoadingAtom.startLoading();
  try {
    const users = await api.users.list();
    setUsers(users);
  } finally {
    usersLoadingAtom.stopLoading();
  }
}

fetchingAtom<DataType, PaginationType>(key, defaultValue?, defaultFetching?)

A full fetch lifecycle in one atom. Value shape: { isLoading, data, error, pagination? }. Actions: startLoading, stopLoading, success(data, pagination?), failed(error), append(data), prepend(data). Hooks: useLoading(), useData(), useError(), usePagination(). See the paginated fetch recipe below for a full flow.

defaultFetching is true by default — the atom starts in a loading state so the first render shows a spinner even before startLoading() is called.

fetchingAtom is for one-off fetch flows, not a query cache. It has no key-based deduping, no automatic refetch, no stale-while-revalidate. For server-state caching with invalidation, refetch-on-focus, and optimistic updates, use @mongez/atomic-query.

portalAtom<T>(name, opened?)

A coordinator atom for modals, drawers, tooltips, dropdowns. Holds { opened: boolean, data: T } and exposes open(data?), close(), toggle(data?), useOpened(), useData(). See the modal recipe below for a full trigger/consumer pair.

The portal atom's internal key is suffixed with -portal. portalAtom("deleteUser") registers an atom whose key is deleteUser-portal. This is only visible when hydrating by key — the variable you exported still works as expected.


SSR — <AtomStoreProvider>

The module-level atoms registry is shared per Node process. Two concurrent SSR requests on the same server would see each other's mutations — a classic SSR state-leakage bug. <AtomStoreProvider> solves this by giving each request its own scoped clone of every atom touched inside it.

import { AtomStoreProvider } from "@mongez/react-atom";

<AtomStoreProvider>
  <App />
</AtomStoreProvider>

Without a provider, atoms fall back to the module-level singleton — the correct default for client-only SPAs.

Props

type AtomStoreProviderProps = {
  store?: AtomStore;                       // bring your own; otherwise auto-created
  initialAtoms?: Atom<any>[];              // pre-register these in the store
  initialValues?: Record<string, unknown>; // hydration values keyed by atom key
  children: React.ReactNode;
};

| Prop | Behaviour | Note | |---|---|---| | store | An existing AtomStore to use. | Caller owns the lifecycle — the provider does NOT destroy it on unmount. | | initialAtoms | Atom templates to pre-register on first render. | Required if you want initialValues to apply to atoms that no component has touched yet. | | initialValues | Initial values keyed by atom key. | Applied via silent update so the first render of subscribers sees the hydrated values — no flash. |

When store is omitted, the provider auto-creates one and destroys it on unmount.

Per-request isolation

Two providers in the same tree are fully independent — useful for multi-tenant dashboards, A/B splits, or per-route boundaries.

<>
  <AtomStoreProvider>
    <TenantA />
  </AtomStoreProvider>
  <AtomStoreProvider>
    <TenantB />
  </AtomStoreProvider>
</>

useAtom(template | key) and useAtomStore()

useAtom<V, A>(template: Atom<V, A>): Atom<V, A>;     // → store-scoped clone (or template if no provider)
useAtom<V>(key: string): Atom<V> | undefined;        // → scoped atom by key, or undefined
useAtomStore(): AtomStore | null;                    // → the active store

The per-atom hooks (useValue, useState, use, useWatch) already do this resolution internally — you only need useAtom(template) explicitly when calling an action method from an event handler under SSR. See the SSR-safe action recipe below. The string overload is a legacy escape hatch left over from v5; prefer the template form.

useAtomStore() returns the active AtomStore or null — reach for it when you need to snapshot on demand, manually hydrate from a custom transport, or integrate with a non-React layer.

Hydration helpers

serializeSnapshot(snapshot, options?): string
serializeStore(store, options?): string
readHydration(id?: string): Record<string, unknown> | null

<HydrateAtomsScript snapshot={…} id?={…} nonce?={…} />

| Helper | Purpose | |---|---| | serializeSnapshot | XSS-safe JSON for a <script> tag — escapes </script> and U+2028/U+2029. | | serializeStore | Convenience over serializeSnapshot(store.snapshot()). | | <HydrateAtomsScript> | Renders an inline <script type="application/json"> carrying the snapshot. Place it once per provider. | | readHydration | Client-side picker that parses the script tag. Returns null if missing or malformed. | | DEFAULT_HYDRATION_SCRIPT_ID | The DOM id <HydrateAtomsScript> and readHydration agree on: "__mongez_atom_state". |

Skip these helpers if your framework already has a typed server-to-client transport. Next.js (__NEXT_DATA__), Remix (useLoaderData), and TanStack Start (route loader) all have their own. Feed the snapshot straight into <AtomStoreProvider initialValues={…}> instead of round-tripping through an inline script.


Recipes

Share a user atom across components with granular subscriptions

Reach for this when multiple components consume slices of the same value. useValue() re-renders on every change; use("name") re-renders only when that single key changes — important for fields that update frequently (lastSeen, presence, cursor position).

// atoms/user.ts
import { atom } from "@mongez/react-atom";

type User = { id: number; name: string; email: string; lastSeen: number };

export const currentUserAtom = atom<User | null>({
  key: "currentUser",
  default: null,
  actions: {
    async signIn(email: string, password: string) {
      this.update(await api.auth.signIn({ email, password }));
    },
    signOut() { this.update(null); },
  },
});
// components/Header.tsx — re-renders on ANY field change
import { currentUserAtom } from "../atoms/user";

export function Header() {
  const user = currentUserAtom.useValue();
  if (!user) return <a href="/login">Sign in</a>;
  return <span>Hello, {user.name}</span>;
}

// components/AccountEmail.tsx — re-renders ONLY when `email` changes.
// Frequent `lastSeen` updates don't wake this component.
export function AccountEmail() {
  const email = currentUserAtom.use("email");
  return <small>{email}</small>;
}

export function SignOutButton() {
  return <button onClick={() => currentUserAtom.signOut()}>Sign out</button>;
}

Open a modal from anywhere with portalAtom

Reach for this when a modal, drawer, or dialog opens from list rows, header buttons, or keyboard shortcuts and you don't want to lift its open/close state to a common ancestor. The trigger and the modal don't need to share a parent — the atom is the bridge.

// atoms/modals.ts
import { portalAtom } from "@mongez/react-atom";

export const editUserModal = portalAtom<{ id: number; initialName: string }>("editUser");
// components/UserRow.tsx — trigger
import { editUserModal } from "../atoms/modals";

function UserRow({ user }) {
  return (
    <tr>
      <td>{user.name}</td>
      <td>
        <button onClick={() => editUserModal.open({ id: user.id, initialName: user.name })}>
          Edit
        </button>
      </td>
    </tr>
  );
}
// components/EditUserModal.tsx — render once at the app root
import { editUserModal } from "../atoms/modals";

export function EditUserModal() {
  const opened = editUserModal.useOpened();
  const data = editUserModal.useData();
  if (!opened) return null;
  return (
    <Dialog onClose={() => editUserModal.close()}>
      <h2>Edit user</h2>
      <form onSubmit={async (e) => {
        e.preventDefault();
        await api.users.update(data.id, /* ... */);
        editUserModal.close();
      }}>
        <input defaultValue={data.initialName} />
        <button>Save</button>
      </form>
    </Dialog>
  );
}

Wire a paginated fetch with fetchingAtom

Reach for this when a single endpoint feeds a single piece of UI and you don't need cache invalidation or refetch — settings forms, dashboard panels, detail pages. success, failed, append, and prepend each set isLoading: false in one merge — no finally wrapper needed.

// atoms/products.ts
import { fetchingAtom } from "@mongez/react-atom";

type Product = { id: number; name: string; price: number };
type Pagination = { page: number; pages: number };

export const productsAtom = fetchingAtom<Product[], Pagination>("products");

export async function loadProducts(page = 1) {
  productsAtom.startLoading();
  try {
    const { data, pagination } = await api.products.list({ page });
    productsAtom.success(data, pagination);
  } catch (err) {
    productsAtom.failed(err);
  }
}
// components/ProductsList.tsx
import { productsAtom, loadProducts } from "../atoms/products";

export function ProductsList() {
  const isLoading = productsAtom.useLoading();
  const products = productsAtom.useData();
  const error = productsAtom.useError();

  React.useEffect(() => { loadProducts(); }, []);

  if (error) return <p className="text-red-500">{String(error)}</p>;
  if (!products && isLoading) return <Spinner />;
  return <ul>{products?.map(p => <li key={p.id}>{p.name} — ${p.price}</li>)}</ul>;
}

Hydrate atoms in SSR (Next.js App Router)

Reach for this when you need the first client render to match the server HTML — current user, feature flags, initial page data. Without hydration the client starts from default and renders a loading state (or a different UI than the server), causing a hydration mismatch or visible flash.

// atoms/user.ts — shared between server and client
import { atom } from "@mongez/react-atom";

type User = { id: number; name: string; role: "admin" | "viewer" } | null;
export const currentUserAtom = atom<User>({ key: "currentUser", default: null });
// app/layout.tsx — SERVER component
import { createAtomStore } from "@mongez/atom";
import { AtomStoreProvider, HydrateAtomsScript } from "@mongez/react-atom";
import { getCurrentUser } from "@/lib/auth";
import { currentUserAtom } from "@/atoms/user";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const user = await getCurrentUser();          // server-only round-trip
  const store = createAtomStore();
  store.use(currentUserAtom).update(user);      // pre-fill the store

  return (
    <html lang="en">
      <body>
        <AtomStoreProvider store={store}>
          {children}
          <HydrateAtomsScript snapshot={store.snapshot()} />
        </AtomStoreProvider>
      </body>
    </html>
  );
}
// app/providers.tsx — CLIENT boundary
"use client";
import { AtomStoreProvider, readHydration } from "@mongez/react-atom";

export function ClientProviders({ children }: { children: React.ReactNode }) {
  return (
    <AtomStoreProvider initialValues={readHydration() ?? undefined}>
      {children}
    </AtomStoreProvider>
  );
}

currentUserAtom.useValue() is never null on first render now — the server already fetched it and the client store starts pre-filled. Same pattern works for Remix (load in a loader, pass via useLoaderData) and TanStack Start (route loader plus a client root); skip <HydrateAtomsScript> / readHydration() in those and feed the snapshot straight into initialValues.

Call an action method safely under SSR

Reach for this when an event handler calls an action method (auth.logout(), cart.checkout()) inside a component rendered under <AtomStoreProvider>. Calling the module-level atom directly mutates the template, which is shared across concurrent SSR requests — a leak between users. Use useAtom(template) to grab the per-request scoped clone instead.

import { useAtom } from "@mongez/react-atom";

// Wrong under SSR — mutates the shared module-level template.
function BadLogoutButton() {
  return <button onClick={() => authAtom.logout()}>Log out</button>;
}

// Right — resolves to the per-request scoped clone under a provider.
function LogoutButton() {
  const auth = useAtom(authAtom);
  return <button onClick={() => auth.logout()}>Log out</button>;
}

On the client (no provider), useAtom(template) falls back to the template itself, so the safe form works everywhere. The per-atom hooks (useValue, useState, use, useWatch) already do this resolution internally — you only need useAtom(template) when calling action methods outside a hook.

Isolate atom state per test

Reach for this in unit tests when you want every test to start from a clean slate. Wrapping each render in <AtomStoreProvider> gives every test its own scoped store — the module-level template stays untouched between runs.

// __tests__/UserBadge.test.tsx
import { render, screen } from "@testing-library/react";
import { AtomStoreProvider } from "@mongez/react-atom";
import { currentUserAtom } from "../atoms/user";
import { UserBadge } from "../components/UserBadge";

test("renders user name when atom is pre-filled", () => {
  render(
    <AtomStoreProvider
      initialValues={{ currentUser: { id: 1, name: "Alice", email: "[email protected]" } }}
    >
      <UserBadge />
    </AtomStoreProvider>,
  );
  expect(screen.getByText("Alice")).toBeInTheDocument();
});

test("renders 'Guest' when atom is at default", () => {
  render(
    <AtomStoreProvider>
      <UserBadge />
    </AtomStoreProvider>,
  );
  expect(screen.getByText("Guest")).toBeInTheDocument();
});

For a quick subtree pre-fill without an entire AtomStoreProvider, <currentUserAtom.Provider value={…}> also works — but it pushes the value via useEffect, so the first frame still reads the previous value. <AtomStoreProvider initialValues={…}> applies synchronously before first render.

Enable Redux DevTools

Reach for this in development builds when you want to see every atom update in the Redux DevTools extension — preset atoms, fetch atoms, and portal atoms all show up.

// app entry — runs once at boot
import { enableAtomDevtools } from "@mongez/atom";

if (process.env.NODE_ENV !== "production") {
  enableAtomDevtools({ name: "MyReactApp" });
}

Time-travel works: rewinding a fetchingAtom from success back to startLoading rolls the connected UsersList back to its spinner state.

Strip the import in production. The bridge subscribes to every atom and serialises every update — fine in dev, wasteful in a shipped bundle. The process.env.NODE_ENV check above is the standard guard.


TypeScript

Atom value and action types flow into every hook automatically. ReactAtom<V, A> extends Atom<V, A> from @mongez/atom with the React-aware actions; type generic helpers that read from an atom via a hook as ReactAtom<V, A> rather than the base Atom<V, A>.

import type {
  ReactAtom,
  ReactActions,
  OpenAtomActions, OpenAtomType,
  LoadingAtom, LoadingAtomActions,
  FetchingAtomType, FetchingAtomActions,
  AtomPortal, PortalActions,
  AtomStoreProviderProps, HydrateAtomsScriptProps,
} from "@mongez/react-atom";

function useFirstChar<V extends { name: string }>(atom: ReactAtom<V>) {
  return atom.use("name")[0];   // `use("nope")` would be a compile error
}

Migration

The v5 → v6 jump moves the package to React 18+ only and rebuilds every hook on useSyncExternalStore for tear-free concurrent reads. The <AtomStoreProvider> surface is new; legacy <AtomProvider> / AtomContext survive as deprecated shims that translate to the new types. Full diff and step-by-step migration in MIGRATION.md.


Related packages

| Package | Use when you need | |---|---| | @mongez/atom | The framework-agnostic atom primitive this package wraps. Use it directly in vanilla / Vue / non-React code, or alongside this package for stores, derive, and persistence. | | @mongez/atomic-query | Server-state cache for React with query keys, invalidation, refetch-on-focus, and optimistic updates. Built on top of this package — use it instead of hand-rolled fetchingAtom flows once you outgrow them. | | @mongez/events | Cross-feature pub/sub. The atom's onChange covers most local subscription needs; reach for events when multiple unrelated features need to coordinate. | | @mongez/cache | Pluggable cache facade — drop into @mongez/atom's persist slot for free localStorage / encrypted / runtime persistence on atoms shared with this package. |


Further reading

  • MIGRATION.md — v5 → v6 transition guide.
  • CHANGELOG.md — release notes.
  • llms-full.txt — exhaustive single-file API surface for tool-assisted development.
  • skills/ — per-topic deep-dives (atoms, presets, SSR, recipes).

License

MIT