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

dev-react-microstore

v8.1.0

Published

A minimal global state manager for React with fine-grained subscriptions.

Readme

dev-react-microstore

Probably the fastest store library ever created for React.

A minimal, zero-dependency global state manager with fine-grained subscriptions, full TypeScript inference, and a tiny footprint (~2.6KB gzipped).

Benchmarks

Update throughput vs the latest Zustand, Jotai, Valtio, and nanostores — vanilla store engine, ops/sec, higher is faster:

| operation | microstore | zustand | nanostores | valtio | jotai | |---|---|---|---|---|---| | update a value + notify | 27.7M | 15.6M | 13.0M | 3.6M | 1.6M | | functional update (x => x + 1) | 27.8M | 15.5M | 12.9M | 3.4M | 1.4M | | fine-grained update (1 of 1000) | 27.8M | 14.8M † | 13.0M | 39K ‡ | 1.6M | | notify 1000 subscribers | 182K | 111K | 56K | — | 35K |

microstore leads the update path: it mutates state in place and notifies with a tight loop, while Zustand allocates a fresh state object per update, Jotai carries atom-graph overhead, and Valtio pays proxy cost. Run npm run bench to reproduce (src/vs.bench.ts). These are store-engine numbers; in React, all five re-render only the components whose data actually changed.

On store creation and subscribe/unsubscribe microstore is mid-pack rather than first — the update path is where it leads. † Zustand's fine-grained row uses separate stores; its single-store selector pattern is O(N). ‡ Valtio batches notifications asynchronously and its per-key subscribe is O(N).

Installation

npm install dev-react-microstore

Quick Start

import { createStoreState, createSelectorHook } from 'dev-react-microstore';

const store = createStoreState({
  count: 0,
  user: { name: 'Alice', age: 30 },
});

export const useStore = createSelectorHook(store);

function Counter() {
  const { count } = useStore(['count']);
  return <button onClick={() => store.increment('count')}>{count}</button>;
}

One line to create the hook, with all types inferred from the store instance and no manual generics.

API

createStoreState(initialState, options?)

Creates a reactive store. options.equals registers per-key equality up front (the declarative form of skipSetWhen). Returns a StoreType<T>:

| Method | Description | |--------|-------------| | get() | Returns the full state object | | getKey(key) | Returns the value of a single key | | set(partial) | Partially updates state | | setKey(key, value) | Sets a single key | | update(key, updater) | Sets a key from its current value: update(key, prev => next) | | increment(key, amount?) | Adds amount (default 1) to a numeric key | | toggle(key) | Flips a boolean key | | merge(key, partial) | Returns { ...state[key], ...partial } without writing | | mergeSet(key, partial) | Shallow-merges and writes to the store | | reset(keys?) | Resets to initial values. No args = full reset | | batch(fn) | Groups updates so listeners fire once at the end | | subscribe(keys, listener) | Subscribe to specific keys. Returns unsubscribe function | | select(keys) | Returns a snapshot of specific keys | | onChange(keys, callback) | Non-React listener with (newValues, prevValues) | | derive(outKey, inputs, compute) | Compute a key from other keys, recomputed in the same set that changes an input | | addMiddleware(fn, keys?) | Intercept, block, or transform updates | | skipSetWhen(key, fn) | Custom equality: skips the update when fn(prev, next) returns true | | removeSkipSetWhen(key) | Remove custom equality for a key |

createSelectorHook(store)

Returns a pre-bound React hook with full type inference:

const useStore = createSelectorHook(store);

const { user } = useStore(['user']);
// user is { name: string; age: number }

useStoreSelector(store, selector)

Low-level React hook. Prefer createSelectorHook for cleaner usage. The selector is an array of keys, optionally with per-key equality functions:

// Key array — subscribes to those keys, returns a { key: value } slice
const { count, name } = useStoreSelector(store, ['count', 'name']);

// Per-key custom equality — re-render only when your comparison reports a change
const { tasks } = useStoreSelector(store, [
  { tasks: (prev, next) => prev.length === next.length },
]);

useStoreKey(store, key)

Subscribes to a single key and returns its value directly, with no wrapper object. It's the lean path for the common one-key case, and re-renders only when that key changes (honoring options.equals / skipSetWhen).

const count = useStoreKey(store, 'count');

StoreType<T>

The type of a store instance. Import it to hold a store as a field (compose stores) or pass one around:

import { createStoreState, type StoreType } from 'dev-react-microstore';

interface Row { store: StoreType<RowState>; /* … actions, refs … */ }

createStore(initialState, defineActions, options?)

A store with its actions bundled on .actions, so the functions that mutate state live with the store instead of scattered module setters. Everything on StoreType<T> (get, set, subscribe, …) is still available.

const counter = createStore({ count: 0 }, (store) => ({
  inc: () => store.set({ count: store.get().count + 1 }),
  reset: () => store.reset(['count']),
}));

counter.actions.inc();
const { count } = useStoreSelector(counter, ['count']);

createStoreFamily(init, options?): stores in stores

A keyed collection of stores for per-item state (grid rows, per-entity slices). Each id gets its own store with its own fine-grained subscriptions, so updating one item re-renders only that item. get is a pure, memoized, render-safe accessor.

const rows = createStoreFamily((id: string) => ({ selected: false, label: '' }));

// mutate one item
rows.get('a').set({ selected: true });

// a row component subscribes to its OWN sub-store — siblings don't re-render
function Row({ id }: { id: string }) {
  const { selected } = useStoreSelector(rows.get(id), ['selected']);
  return <Cell active={selected} />;
}

For a single-key row, useStoreKey(rows.get(id), 'selected') is even leaner: it returns the value directly, with no wrapper object.

options may be static or a per-id function ((id) => StoreOptions). Lifecycle: has(id), remove(id) (a later get starts fresh), keys(), size(), clear(). Keep the list of live ids in your own store (you usually already have it) and read each item through its sub-store.

merge vs mergeSet

merge returns the merged object without touching the store:

const updated = store.merge('user', { age: 31 });
// updated = { name: 'Alice', age: 31 }
// store is unchanged

mergeSet writes it:

store.mergeSet('user', { age: 31 });
// store.user is now { name: 'Alice', age: 31 }

Both are type-safe. Calling on a primitive key is a compile error.

Batching

Group multiple updates so listeners fire once:

store.batch(() => {
  store.setKey('count', 10);
  store.mergeSet('user', { age: 25 });
  store.setKey('name', 'Bob');
});

reset

store.reset();              // Full reset to initial state
store.reset(['count']);      // Reset specific keys

Convenience setters

update sets a key from its current value; increment and toggle are typed to numeric and boolean keys:

store.update('count', (c) => c + 1);   // functional update
store.increment('count');              // += 1
store.increment('count', 5);           // += 5  (a negative amount decrements)
store.toggle('open');                  // flip a boolean

store.increment('open');  // ✗ compile error — 'open' is a boolean
store.toggle('count');    // ✗ compile error — 'count' is a number

All three go through setKey, so they respect middleware, equality, and batching.

derive: computed keys

Derive a key from other keys. compute runs synchronously inside any set that changes an input and commits its result in the same update, so components subscribed to the derived key re-render only when its value actually changes:

const store = createStoreState({ first: 'Ada', last: 'Lovelace', full: '' });
store.derive('full', ['first', 'last'], ({ first, last }) => `${first} ${last}`);

store.setKey('last', 'Byron');
store.getKey('full'); // 'Ada Byron'

compute receives only the declared inputs. Returns a disposer that removes the derivation.

Middleware

Intercept, block, or transform updates:

// Validation — block negative counts
store.addMiddleware(
  (state, update, next) => {
    if (update.count !== undefined && update.count < 0) return;
    next();
  },
  ['count']
);

// Transform
store.addMiddleware((state, update, next) => {
  if (update.user?.name) {
    next({ ...update, user: { ...update.user, name: update.user.name.trim() } });
  } else {
    next();
  }
});

// Logging
store.addMiddleware((state, update, next) => {
  console.log('Update:', update);
  next();
});

Persistence

Built-in middleware for automatic state persistence. Supports both sync and async storage:

import { createStoreState, createPersistenceMiddleware, loadPersistedState } from 'dev-react-microstore';

// Sync (localStorage / sessionStorage)
const persisted = loadPersistedState<AppState>(localStorage, 'app', ['theme', 'user']);
const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
store.addMiddleware(createPersistenceMiddleware(localStorage, 'app', ['theme', 'user']));

// Async (React Native AsyncStorage)
const persisted = await loadPersistedState<AppState>(AsyncStorage, 'app', ['theme', 'user']);
const store = createStoreState<AppState>({ theme: 'light', user: null, ...persisted });
store.addMiddleware(createPersistenceMiddleware(AsyncStorage, 'app', ['theme', 'user']));

Each key is stored individually (app:theme, app:user).

onChange

Listen for value changes outside React:

const unsub = store.onChange(['theme', 'locale'], (values, prev) => {
  document.body.className = values.theme;
});

unsub();

Custom Comparison

Control when re-renders happen:

const { tasks } = useStore([
  {
    tasks: (prev, next) =>
      !prev.some((t, i) => t.completed !== next?.[i]?.completed)
  }
]);

skipSetWhen

Custom equality per key, skipping updates when values are semantically equal:

const store = createStoreState({ user: { id: 1, name: 'Alice' }, tags: ['a', 'b'] });

store.skipSetWhen('user', (prev, next) => prev.id === next.id && prev.name === next.name);
store.skipSetWhen('tags', (prev, next) => prev.length === next.length && prev.every((t, i) => t === next[i]));

store.mergeSet('user', { name: 'Alice' }); // skipped — same content

store.removeSkipSetWhen('user'); // back to reference equality

Features

  • Fine-grained subscriptions: components only re-render when their keys change
  • Full TypeScript inference: no manual generics
  • createSelectorHook for one-line per-store hooks
  • useStoreKey for single-key subscriptions that return the value directly
  • merge / mergeSet for ergonomic object updates
  • update / increment / toggle convenience setters, typed to the right key kinds
  • batch to group updates
  • reset to restore initial state (full or per-key)
  • derive for computed keys, recomputed only when their inputs change
  • onChange for non-React listeners
  • skipSetWhen for custom equality
  • Custom comparison functions in selectors
  • Middleware (validation, transforms, logging)
  • Persistence (localStorage, sessionStorage, AsyncStorage)
  • Zero dependencies (peer: React >= 17)
  • ~2.6KB gzipped

ESLint rule

The package ships an ESLint rule, no-unused-selector-keys, that flags selector keys you never destructure, with no separate plugin to install. Requires ESLint 9+ (flat config):

// eslint.config.js
import microstore from 'dev-react-microstore/eslint';

export default [{
  plugins: { 'react-microstore': microstore },
  rules: { 'react-microstore/no-unused-selector-keys': 'warn' },
}];
// warns — 'b' is selected but never destructured
const { a } = useStore(['a', 'b']);

// fine
const { a, b } = useStore(['a', 'b']);

Selecting a key that doesn't exist is already a TypeScript error, so this rule covers the one thing types can't: keys you asked for and never used. It handles useStoreSelector, hooks bound via createSelectorHook, and { key: compareFn } custom-compare entries. For selector hooks bound in another file, list their names:

rules: {
  'react-microstore/no-unused-selector-keys': ['warn', { hooks: ['useAppStore'] }],
}