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

@supercat1337/store2

v2.0.8

Published

A lightweight, efficient, and easy-to-use reactive state management system for JavaScript applications.

Readme

@supercat1337/store2

A lightweight, efficient, and fully reactive state management library for JavaScript.

npm version License: MIT Bundle Size


Why store2?

  • Lightweight — Zero runtime dependencies (only @supercat1337/event-emitter under the hood)
  • MobX‑inspired DX — Transparent reactive graph, but with a smaller footprint
  • Framework‑agnostic — Works with vanilla JS, React, Vue, Svelte, or any DOM environment
  • Predictable batching — Updates are batched by default, no subtle race conditions
  • Full TypeScript support — Typed via JSDoc, no separate @types package needed

Features

  • MobX-inspired mental model – predictable transparent reactive graph, but lightweight and dependency‑free (only one tiny dependency: @supercat1337/event-emitter).
  • Reactive primitivesAtom, Computed, Collection, ShallowReactive.
  • Reactive containersStore (key‑value store) and ReactiveList (array‑like list).
  • Declarative APIsautorun, reaction, when, waitUntil.
  • Batched updates – group changes with batch() to reduce notifications.
  • Observable objectsmakeObservable, makeAutoObservable, extendObservable.
  • Promise integrationfromPromise to observe pending/resolved/rejected states.
  • Tiny and fast – only one external dependency (event emitter), fully typed via JSDoc.

Installation

npm install @supercat1337/store2

Quick Start

import { atom, computed, autorun, batch } from '@supercat1337/store2';

// Create reactive atoms
const price = atom(10);
const quantity = atom(2);

// Compute total reactively
const total = computed(() => price.value * quantity.value);

// Autorun – runs whenever its dependencies change
autorun(() => {
    console.log(`Total: ${total.value}`);
});
// Logs: Total: 20

// Update state – autorun fires automatically
price.value = 15; // Logs: Total: 30
quantity.value = 3; // Logs: Total: 45

// Batch multiple updates – only one notification
batch(() => {
    price.value = 20;
    quantity.value = 4;
});
// Logs: Total: 80 (only once)

Simple DOM Binding Example

const count = atom(0);
const btn = document.getElementById('counter-btn');
const display = document.getElementById('display');

autorun(() => {
    display.textContent = `Clicks: ${count.value}`;
});

btn.addEventListener('click', () => count.value++);

Examples

Check out the examples/ folder for runnable demos:

  • Counter — Basic atom + DOM binding
  • Todos — Reactive list with Collection
  • Autorunautorun, reaction, and when in action
  • Sum — Computed values with multiple dependencies
  • TodoMVC — Full-featured TodoMVC implementation

Core Concepts

| Primitive | Purpose | Example | | ------------------- | ------------------------- | ------------------------------------------------- | | atom() | Single mutable value | const count = atom(0) | | computed() | Derived value (cached) | const double = computed(() => count.value * 2) | | collection() | Reactive array | const items = collection([1, 2, 3]) | | shallowReactive() | Reactive object (shallow) | const state = shallowReactive({ name: 'Alex' }) |

Atoms

An Atom holds a single value. It is the most basic reactive unit.

const count = atom(0);
count.subscribe(() => console.log('count changed:', count.value));
count.value++; // triggers the subscriber

📖 Full documentation: Atom

Computed

A Computed derives its value from other reactive sources. It caches the result and updates only when dependencies change.

const a = atom(2);
const b = atom(3);
const sum = computed(() => a.value + b.value);
console.log(sum.value); // 5
a.value = 5; // sum is automatically recalculated
console.log(sum.value); // 8

📖 Full documentation: Computed

Collection

A Collection wraps an array and makes its mutations (push, pop, splice, index assignment) reactive.

import { collection } from '@supercat1337/store2';

const items = collection([1, 2, 3]);
items.subscribe(() => console.log('array changed'));
items.value.push(4); // triggers notification
console.log(items.value); // [1, 2, 3, 4]

📖 Full documentation: Collection

ShallowReactive

shallowReactive turns a plain object into a reactive proxy. Only direct property changes are tracked (nested objects are not made reactive).

import { shallowReactive } from '@supercat1337/store2';

const state = shallowReactive({ name: 'Alice', age: 30 });
state.subscribe(() => console.log('state updated'));
state.age = 31; // triggers notification

📖 Full documentation: ShallowReactive


⚠️ Important: Nested Mutations Are Not Tracked

store2 uses reference equality (===) by default to detect changes. This means:

const user = atom({ name: 'Alex', age: 25 });
user.value.age = 26; // ❌ Does NOT trigger reactivity

Always use immutable updates:

user.value = { ...user.value, age: 26 }; // ✅ Triggers reactivity

For deeply nested structures, consider:

  • Atomization – split state into multiple atoms:
    const userName = atom('Alex');
    const userAge = atom(25);
  • makeAutoObservable – for classes with nested objects:
    class User {
        name = 'Alex';
        profile = { age: 25 };
        constructor() {
            makeAutoObservable(this);
        }
    }
    const user = new User();
    user.profile.age = 26; // ✅ Works!

📖 See the full documentation on deep objects for more details.


Reactive Containers

Store

Store is a key‑value container that can hold any reactive items (atoms, computeds, collections, other stores). It batches updates and notifies subscribers about changes.

import { Store, atom, computed } from '@supercat1337/store2';

const store = new Store();
const x = atom(1);
const y = atom(2);
const z = computed(() => x.value + y.value);

store.addItems({ x, y, z });

store.subscribe(updates => {
    console.log('Changed:', Array.from(updates.keys()));
});

x.value = 10; // triggers subscriber with updates: ['x', 'z']

You can also mute/unmute notifications temporarily:

store.muteUpdates();
x.value = 100;
y.value = 200;
store.unmuteUpdates(); // only one notification with both changes

📖 Full documentation: Store

ReactiveList

ReactiveList is a reactive array‑like list. It automatically wraps primitives in Atom and objects in ShallowReactive. It provides methods to add, remove, update, and clear items.

import { ReactiveList } from '@supercat1337/store2';

const list = new ReactiveList();
list.subscribe(() => console.log('list changed'));

list.add(1, 2, 3); // primitives → Atoms
list.setItem(1, 42); // update value at index 1
list.removeItem(0); // remove first element
console.log(list.toArray()); // [42, 3]

📖 Full documentation: ReactiveList


Advanced APIs

autorun(fn, options)

Runs fn immediately and re‑runs it whenever any reactive value used inside changes. Dependencies are collected only during the first run.

const a = atom(1);
const b = atom(2);
autorun(() => {
    console.log(a.value + b.value);
});
// Output: 3
a.value = 5; // Output: 7

reaction(dataFn, effectFn, options)

Tracks dependencies inside dataFn and runs effectFn whenever those dependencies change.

reaction(
    () => [a.value, b.value],
    () => console.log('a or b changed')
);

batch(fn)

Groups multiple updates into a single notification. Nested batches are supported.

batch(() => {
    a.value = 10;
    b.value = 20;
});
// only one notification (if any subscriber exists)

when(predicate, effect, options)

Waits for predicate to become true, then runs effect once and automatically unsubscribes.

const ready = atom(false);
when(
    () => ready.value === true,
    () => {
        console.log('Ready!');
    }
);
ready.value = true; // logs "Ready!"

waitUntil(predicate, options)

Returns a promise that resolves when predicate becomes true.

await waitUntil(() => dataLoaded.value === true);
console.log('Data loaded');

fromPromise(promise)

Observes a promise’s state (pending, resolved, rejected) and lets you react to each phase.

const promise = fetch('/api/data');
const observable = fromPromise(promise);

observable.case({
    pending: () => console.log('Loading...'),
    resolved: data => console.log('Data:', data),
    rejected: err => console.error('Error:', err),
});

makeObservable, makeAutoObservable, extendObservable

These functions add reactivity to existing objects or classes. makeAutoObservable automatically infers which properties should be reactive.

class Counter {
    value = 0;
    get double() {
        return this.value * 2;
    }
    increment() {
        this.value++;
    }
}

const counter = new Counter();
makeAutoObservable(counter);

autorun(() => {
    console.log('Double:', counter.double);
});
counter.increment(); // logs "Double: 2"

📖 Full API documentation: docs-md/README.md


Migration from MobX (Quick Reference)

| MobX | store2 | | -------------------------- | -------------------------- | | observable({ ... }) | shallowReactive({ ... }) | | computed(() => ...) | computed(() => ...) | | autorun(() => ...) | autorun(() => ...) | | action(() => ...) | batch(() => ...) | | makeAutoObservable(this) | makeAutoObservable(this) | | observable([]) | collection([]) | | observable.map() | Store |


Important Notes / Known Limitations

  • Static dependency collection in autorun and reaction
    Dependencies are captured only once – during the first execution of the tracked function. If your function conditionally uses different reactive items (e.g., inside an if statement), changes to items not used in the first run will not trigger the effect.
    Workaround: Use computed to pre‑compute conditional values, or restructure your effect so that all possible dependencies are accessed during the first run (e.g., by reading them unconditionally).

  • Atom clones objects shallowly
    When you assign an object/array to an Atom, it is shallow‑cloned (Object.assign or slice). Mutating nested properties will not trigger reactivity. Use Collection or ShallowReactive for nested structures.

  • Collection and ShallowReactive return Proxies
    The .value property of a Collection and the result of shallowReactive() are reactive Proxies. Direct mutations via the proxy are tracked; using the raw underlying value (via .getRawValue()) breaks reactivity.

  • Destructuring breaks reactivity
    When using shallowReactive or accessing properties of a Collection, destructuring fields (e.g., const { name, age } = state) breaks reactivity for those variables. Always access properties directly through the reactive object (e.g., state.age) to ensure dependencies are tracked correctly.

  • Error handling in Computed
    If a Computed function throws an error, the error is caught and stored. The computed will re‑throw the same error until its dependencies change, at which point it will try to recompute.

  • Destroyed items
    Calling destroy() on a reactive item cleans up all subscriptions and dependencies. Further operations (except checking isDestroyed) will throw an error.


TypeScript Support

This library is written in plain JavaScript with JSDoc annotations. Type definitions are generated automatically and shipped with the package. You get full IntelliSense and type checking in supporting editors.


Documentation & Examples

  • Full API Documentation: docs-md/README.md
  • Examples: Check out the examples/ folder for runnable code snippets covering all features.

License

MIT © 2025–2026 Albert Bazaleev


Links