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

jotai-statewatch

v1.0.5

Published

Lightweight generic atom watcher utilities for Jotai store

Downloads

7

Readme

jotai-statewatch

Lightweight, type‑safe watcher + callback orchestration utilities for Jotai stores. It helps you declaratively wire side‑effects to atom state transitions (including multi‑atom fan‑in conditions, debounced reactions, periodic intervals gated by state, and lifecycle teardown) without scattering ad‑hoc useEffect logic across your app.

ESM‑only. Requires Node >=16 (or a modern bundler / React Native Metro). Ships pure, tree‑shakable code + TypeScript declarations.


✨ Features

  • Deterministic watcher layer – Wraps atoms and tracks current/previous values & change flags.
  • Multi‑atom callbacks – React to combinations of atom changes in a single cohesive function.
  • Interval helpers – Run gated periodic tasks only when conditions are met; auto‑suspend when not.
  • Fan‑in aggregators – Consolidate multiple watcher events into a single typed event object.
  • Typed IDs / config map – Compile‑time safety for watcher identifiers and callback wiring.
  • Explicit teardown – Cleanly stop intervals, release resources, or unsubscribe.
  • Debounce / mutex friendly – Designed to play nicely with lightweight locks like nano-mutex.
  • No runtime dependency on React – Works with Jotai store instances anywhere (web, RN, server scripts for pre‑warming, etc.).

📦 Installation

npm install jotai-statewatch
# or
pnpm add jotai-statewatch
# or
yarn add jotai-statewatch

Peer dependency:

"peerDependencies": { "jotai": "^2.4.0" }

If you are in a CommonJS environment and see ERR_REQUIRE_ESM, switch to dynamic import:

const mod = await import("jotai-statewatch");

🧠 Core Concepts

| Concept | Description | | ----------------- | -------------------------------------------------------------------------------------------------------------- | | Watcher | A wrapped atom that exposes { current, previous, isChanged }. | | Watcher Map | Object literal mapping stable watcher IDs → source atoms. Generates types. | | Callback Config | Declarative description: which watcher IDs it cares about, the async callback, optional teardown, description. | | Interval Callback | Utility that produces a watcher callback driven by a condition + periodic action. | | Watcher Manager | Orchestrates registering / deregistering callbacks and dispatching events. |


🚀 Quick Start

import { atom } from "jotai";
import {
  defineWatchers,
  defineWatcherCallback,
  createWatcherManager,
} from "jotai-statewatch";

// 1. Define atoms
const countAtom = atom(0);
const thresholdAtom = atom(10);

// 2. Build typed watcher map
const watcherMap = {
  countWatcher: countAtom,
  thresholdWatcher: thresholdAtom,
} as const;

const { create: createWatchers, WatcherIds } = defineWatchers(watcherMap);

// 3. Define callback reacting to both atoms
const logWhenExceeded = defineWatcherCallback({
  watchers: [WatcherIds.countWatcher, WatcherIds.thresholdWatcher] as const,
  description: "Logs when count surpasses threshold",
  async callback(events) {
    const { countWatcher, thresholdWatcher } = events;
    if (!countWatcher.isChanged) return; // Only act on count change
    if (countWatcher.current > thresholdWatcher.current) {
      console.log("Threshold exceeded!", {
        count: countWatcher.current,
        threshold: thresholdWatcher.current,
      });
    }
  },
});

// 4. Create runtime watchers + manager
const runtimeWatchers = createWatchers();
const manager = createWatcherManager(runtimeWatchers, [logWhenExceeded]);

// 5. Wire to a jotai store (root store or derived)
import { unstable_createStore } from "jotai";
const store = unstable_createStore();
manager.start(store); // begins listening

// Later: manager.stop(); // invokes teardowns

🧩 API Overview

defineWatchers(watcherMap)

Creates a factory + strongly typed WatcherIds enum‐like object.

const { create, WatcherIds } = defineWatchers({
  fooWatcher: fooAtom,
  barWatcher: barAtom,
} as const);
const runtimeWatchers = create();

defineWatcherCallback(config)

Defines a callback tied to one or more watcher IDs.

const cb = defineWatcherCallback({
  watchers: [WatcherIds.fooWatcher] as const,
  description: "Do something when foo changes",
  async callback(events) {
    if (events.fooWatcher.isChanged) {
      // ...
    }
  },
  teardown: () => {
    /* optional cleanup */
  },
});

Event object shape (per watcher):

interface WatcherEvent<T> {
  current: T;
  previous: T | undefined;
  isChanged: boolean; // strict referential or primitive change
}

createIntervalCallback({ condition, action, intervalMs, runOnSetup, logger })

Utility that yields { callback, cleanup } so you can wrap it with defineWatcherCallback.

  • condition(events) => boolean | Promise<boolean> determines whether the interval should (continue to) run.
  • action() executes each tick when condition holds.
  • Auto‑pauses when condition returns false.

createFanInAggregator(watchers, handler) (if exported in your build)

Combine multiple watchers into a synthesized event; useful for advanced dedupe or state gating.

registerCallbacks(manager, callbacks)

Batch registration helper (your internal wiring may already abstract this).

createSingleAtomWatcher(atom, id?)

Ad hoc wrapper when you do not need the full map pattern.


⏱ Interval Example

import {
  createIntervalCallback,
  defineWatcherCallback,
} from "jotai-statewatch";

const intervalResult = createIntervalCallback({
  watchers: undefined, // Provided when wrapping with defineWatcherCallback
  condition: async (events) => events.sessionWatcher.current.isActive,
  action: async () => syncHeartbeat(),
  intervalMs: 15_000,
  runOnSetup: false,
  logger: console,
});

export const heartbeatCallback = defineWatcherCallback({
  watchers: [WatcherIds.sessionWatcher] as const,
  description: "Heartbeat while session active",
  callback: intervalResult.callback,
  teardown: intervalResult.cleanup,
});

🔒 Concurrency (using nano-mutex)

For idempotent critical sections:

import { createMutex } from "nano-mutex";
const mutex = createMutex();

const guarded = defineWatcherCallback({
  watchers: [WatcherIds.fooWatcher] as const,
  async callback(events) {
    const release = await mutex.acquire();
    try {
      // perform serialized operations
    } finally {
      release();
    }
  },
});

🛡 Change Detection Semantics

  • Primitive values: strict !== comparison.
  • Objects / arrays: referential change only (same as === identity). If you mutate in place, the watcher will report isChanged: false; prefer immutable updates.

🧪 Testing Patterns

Because callbacks are plain functions, you can unit test them directly:

test("fires when count changes", async () => {
  const cb = defineWatcherCallback({
    watchers: [WatcherIds.countWatcher] as const,
    async callback(events) {
      expect(events.countWatcher.isChanged).toBe(true);
      expect(events.countWatcher.current).toBe(2);
    },
  });
  // Simulate dispatch: feed synthetic events object
  await cb.callback({
    countWatcher: { current: 2, previous: 1, isChanged: true },
  } as any);
});

🧷 Lifecycle

  1. createWatchers() creates internal bookkeeping wrappers (lazy, cheap).
  2. manager.start(store) subscribes to underlying atoms and begins diff propagation.
  3. Each atom commit triggers event fan‑out; relevant callbacks receive a snapshot of all watched IDs.
  4. manager.stop() unsubscribes & invokes callback teardowns (interval cleanup, etc.).

⚙️ Performance Notes

  • Minimal overhead: only watched atoms are subscribed; unchanged atoms do not trigger deep computations.
  • Event object is allocated once per callback dispatch. Keep callbacks lean & defer heavier work.
  • Debounce bursts with timers or queues inside your callback (pattern shown in repo access sync example in the consuming app).

🧭 Comparison

| Approach | Pros | Cons | | --------------------------- | ---------------------------- | ------------------------------------------------ | | Scattered useEffect hooks | Familiar | Hard to coordinate multi‑atom logic; duplication | | Derived atoms only | Pure & compositional | Side effects still need orchestration layer | | jotai-statewatch | Centralized, typed, testable | Additional abstraction layer |


🔍 Troubleshooting

| Symptom | Possible Cause | Fix | | ------------------------ | ------------------------------ | ----------------------------------------------- | | Callback never fires | Wrong watcher ID list | Ensure as const on watchers array | | isChanged always false | In-place mutation | Use immutable updates / new references | | Interval never runs | condition never returns true | Log inputs; verify dependency watchers | | ESM import error | CommonJS runtime | Use dynamic import() or enable ESM in project |


📑 Type Reference (Condensed)

interface WatcherEvent<T> {
  current: T;
  previous: T | undefined;
  isChanged: boolean;
}

type WatcherEventsMap<Ids extends readonly string[]> = Record<
  Ids[number],
  WatcherEvent<any>
>;

interface WatcherCallbackConfig<Ids extends readonly string[], Events> {
  watchers: Ids;
  description?: string;
  callback: (events: Events) => void | Promise<void>;
  teardown?: () => void | Promise<void>;
}

(Exact generics may differ slightly; see emitted .d.ts files for authoritative signatures.)


🗺 Suggested Project Structure (Example)

statewatch/
  src/
    index.ts
    defineWatchers.ts
    defineWatcherCallback.ts
    createIntervalCallback.ts
    ...
  __tests__/

🔐 Sustainability & Versioning

  • Follows SemVer (MAJOR.MINOR.PATCH).
  • Breaking API changes will increment MAJOR.
  • Minor adds new APIs, patch fixes bugs / types.

🤝 Contributing

  1. Clone / enable pnpm or npm
  2. Run tests: npm test
  3. Add / adjust code + tests
  4. Submit PR with clear description / rationale

Please keep changes small & focused. Add tests for new behavior.


📝 License

MIT © Goban Source


📬 Feedback / Issues

Open an issue: https://github.com/gobansource/jotai-statewatch/issues


[![npm version](https://img.shields.io/npm/v/jotai-statewatch.svg)](https://www.npmjs.com/package/jotai-statewatch)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)

Happy watching!