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

@nice-code/state

v0.67.0

Published

Readme

@nice-code/state

Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms-state.txt (just this package) or nicecode.io/llms.txt (the whole stack) — the complete, current docs flattened into plain text.

Framework-agnostic, Immer-backed state container with fine-grained selector subscriptions, derived reactions, patch streams, and a drop-in React adapter + devtools panel.

Install

bun add @nice-code/state immer

react is an optional peer dependency (>=19) — only needed for the @nice-code/state/react adapter.

Why

  • One immutable store, mutated with Immer — write plain mutations (s.count += 1), get structural sharing for free.
  • No-op updates are genuinely free — listeners only fire when the root reference actually changes.
  • Fine-grained selectors — components and side-effects re-run only when their watched slice changes structurally, not on every update.
  • Tear-free React — the adapter is built on useSyncExternalStore (React 18+ concurrent-safe), no provider/context required.
  • Patches + devtools — every mutation can emit Immer patches; the optional devtools panel gives you a timeline, state inspector, diff view, and revert.

Core concepts

  • Store — owns one immutable state value and a set of listeners.
  • update — mutate the state through an Immer draft.
  • watch — subscribe to a derived slice; fires only on structural change (outside React).
  • createReaction — derive further state from the store's own mutations.
  • useStoreState — subscribe a React component to a store or a selected slice.

Create a store

import { Store } from "@nice-code/state";

interface ICounterState {
  count: number;
  step: number;
}

// Pass a value, or a factory function (re-used by SSR / reset).
export const counterStore = new Store<ICounterState>({ count: 0, step: 1 });

Update state

Mutations run through Immer — mutate the draft freely, the store commits a new immutable state.

// Single update
counterStore.update((s) => {
  s.count += s.step;
});

// Batched — an array of updaters applied in order, committed as one change
counterStore.update([
  (s) => { s.count += 1; },
  (s) => { s.count += 1; },
]);

// Replace the whole state
counterStore.replace({ count: 0, step: 1 });

// Replace by mapping from the current state
counterStore.replaceFromCurrent((s) => ({ ...s, count: 0 }));

The second argument to an updater is a readonly snapshot of the pre-update state:

counterStore.update((draft, original) => {
  draft.count = original.count * 2;
});

Read state

const { count } = counterStore.state;

store.state is the current state, frozen for read safety — reach for it only in plain logic. In components use useStoreState; for side-effects use watch.


React adapter

Import from @nice-code/state/react. No provider needed — pass the store directly.

import { useStoreState } from "@nice-code/state/react";

function Counter() {
  // Select a slice — re-renders only when `count` changes.
  const count = useStoreState(counterStore, (s) => s.count);

  return (
    <button onClick={() => counterStore.update((s) => { s.count += 1; })}>
      {count}
    </button>
  );
}
// No selector → subscribe to the whole state.
const state = useStoreState(counterStore);

Selecting derived/inline values

Selection checks by reference (===) first — perfect for state branches, thanks to Immer's structural sharing. When the reference differs it falls through to an equality function that defaults to deepEqual, so a selector that builds a new object/array each call doesn't re-render on an equal-but-new result:

import { useStoreState } from "@nice-code/state/react";

// New array each call, but only re-renders when the filtered result truly changes.
const activeTodos = useStoreState(store, (s) => s.todos.filter((t) => !t.done));

The deep check only runs on a genuine reference change and is bounded by the selected slice; pass a strict comparator ((a, b) => a === b) to opt back out for a very large slice on a hot path.

Component-local stores

useLocalStore creates a store scoped to a component instance, persisted across renders. Pass a deps array to re-create it (with fresh initial state) when dependencies change.

import { useLocalStore, useStoreState } from "@nice-code/state/react";

function Editor({ docId }: { docId: string }) {
  const store = useLocalStore(() => ({ draft: "" }), [docId]);
  const draft = useStoreState(store, (s) => s.draft);
  // ...
}

Render-prop binding

InjectStoreState subscribes inline without writing a hook:

import { InjectStoreState } from "@nice-code/state/react";

<InjectStoreState store={counterStore} on={(s) => s.count}>
  {(count) => <span>{count}</span>}
</InjectStoreState>

Reactions — derive state from state

A reaction watches a slice and, when it changes, runs a recipe inside Immer to derive further state on the same store. Reactions run before subscribers are notified, so derived fields are always consistent in a single render.

interface IState {
  count: number;
  doubled: number;      // derived — never written by hand
  history: number[];    // derived
}

const store = new Store<IState>({ count: 0, doubled: 0, history: [] });

store.createReaction(
  (s) => s.count,                       // watch
  (count, draft) => {                   // derive
    draft.doubled = count * 2;
    draft.history = [...draft.history, count].slice(-12);
  },
  { runNow: true },                     // run once immediately to seed derived state
);

createReaction returns a disposer to remove the reaction.


Watch — side-effects outside React

watch subscribes to a derived slice and fires the listener only when it changes structurally. Ideal for logging, persistence, or syncing to non-React code.

const unsubscribe = store.watch(
  (s) => s.count,
  (count, allState, previousCount) => {
    console.log(`count: ${previousCount} → ${count}`);
  },
);

The low-level store.subscribe(() => { ... }) fires on every committed update (no slice, no args) — this is the primitive the React adapter builds on.


Patches

Every update can emit Immer patches, enabling undo/redo, sync, and the devtools. Patches are only computed when there's a consumer, so the common path stays allocation-free.

import { applyPatchesToStore } from "@nice-code/state";

// Listen to patches for every update
const stop = store.listenToPatches((patches, inversePatches) => {
  sendToServer(patches);
});

// Or collect patches for a single update
store.update((s) => { s.count += 1; }, (patches, inverse) => {
  undoStack.push(inverse);
});

// Apply patches (e.g. received from elsewhere, or to undo)
store.applyPatches(patches);

Devtools

A zero-config inspector for your stores — timeline of changes, before/after diffs, live state inspector with direct editing, and patch-based revert. Import the panel from @nice-code/state/devtools/browser.

// devtools.ts — register the stores you want to observe
import { StateDevtoolsCore } from "@nice-code/state/devtools/browser";
import { counterStore } from "./counterStore";

export const stateDevtools = new StateDevtoolsCore();
stateDevtools.registerStore("counterStore", counterStore);
// Stream the core to the devtools window; nothing renders inside your app.
import { stateBridgeScope } from "@nice-code/state/devtools/browser";
import { stateDevtools } from "./devtools";

host.contribute(stateBridgeScope(stateDevtools));

The panel — rendered in the standalone devtools window — lets you filter by store, inspect/edit live state, view per-change diffs and patches, pause the timeline, and revert a change via its inverse patches.

Registering a store attaches a patch listener (a negligible dev-time cost). Keep devtools setup out of production bundles.


SSR

useStoreState is built on useSyncExternalStore with a server snapshot, so it renders the store's current value on the server without subscribing. Construct stores with a factory (new Store(() => initialState())) so each request can seed its own state.


Exports

| Entry | Contents | | --- | --- | | @nice-code/state | Store, update, applyPatchesToStore, and all core types | | @nice-code/state/react | useStoreState, useLocalStore, InjectStoreState | | @nice-code/state/devtools/browser | StateDevtoolsCore, NiceStateDevtools |