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

nexus-state

v4.0.0

Published

Lightweight, traceable global state manager for JavaScript and React.

Readme

nexus-state logo

Table of contents

About

Lightweight, framework-agnostic state management with optional actions, React bindings, and traceable updates. Designed for simplicity and performance, with first-class TypeScript inference.

What makes it different:

  • Traceable state. Every update carries a context describing where it came from ("server", "storage", "reset", or your own). That context flows through both middleware and subscribers — so persistence, sync, and devtools can tell a user action from a hydration without guessing.
  • Key-level subscriptions. Subscribers listen to specific keys, so an update only notifies what actually depends on it — no selector is re-run for components that didn't change.
  • Optional React. The core has zero dependencies and no React. Persistence is available from the main entry point; React hooks live behind a separate entry point you opt into.
  • Inference-first types. State and actions are inferred from your config — you rarely write a generic by hand.

Installation

npm install nexus-state

React is an optional peer dependency — only needed if you import nexus-state/react.

| Import | Contents | Needs | | ---------------------- | -------------------------------------- | ------------ | | nexus-state | createNexus, createActs, persist | — | | nexus-state/react | createReactNexus | react (peer) | | nexus-state/devtools | devtools (Redux DevTools adapter) | — |

import { createNexus, createActs, persist } from "nexus-state";
import { createReactNexus } from "nexus-state/react";
import { devtools } from "nexus-state/devtools";

Quick start

You can create two kinds of store — a framework-agnostic core, or a React store with hooks:

import { createNexus } from "nexus-state";

const counter = createNexus({
  state: { count: 0 },
  acts: (get, set) => ({
    increment() {
      set((s) => ({ count: s.count + 1 }));
    },
  }),
});

counter.acts.increment();
counter.get("count"); // number — no generics needed, types are inferred
import { createReactNexus } from "nexus-state/react";

const counter = createReactNexus({
  state: { count: 0 },
  acts: (get, set) => ({
    increment() {
      set((s) => ({ count: s.count + 1 }));
    },
  }),
});

function Counter() {
  const count = counter.use("count"); // re-renders on change
  return <button onClick={counter.acts.increment}>{count}</button>;
}

API

— CORE —
// your-nexus-config
import { createNexus } from "nexus-state";

const nexus = createNexus({
  state: {
    count1: 0,
    count2: 0,
  },

  acts: (get, set) => ({
    increment() {
      set((state) => ({ count1: state.count1 + 1 }));
      this.getState("count1"); // ! calling another action
    },
    getState(value) {
      console.log(`${value}:`, get(value));
    },
  }),
});

export default nexus;

State and action types are inferred from your config, so most stores need no generics:

const nexus = createNexus({
  state: { count: 0 },
  acts: (get, set) => ({
    inc() {
      set((s) => ({ count: s.count + 1 }));
    },
  }),
});

nexus.get("count"); // number
nexus.acts.inc(); // () => void

Pass generics explicitly only when you want to declare the shape up front:

createNexus<MyState, MyActions>({ ... });
// your-nexus-config
import { createNexus, createActs } from "nexus-state";

const counterActs = createActs((get, set) => ({
  increment() {
    set((state) => ({ count1: state.count1 + 1 }));
    this.getState("count1"); // ! calling another action inside
  },
  getState(value) {
    console.log(`${value}:`, get(value));
  },
}));

const nexus = createNexus({
  state: {...},
  acts: counterActs, // ! supports multiple too: [counterActs, otherActs]
});

export default nexus;
type MyState = {...};
type MyActions = {...};

// `this` is typed as the complete acts object across every slice,
// so cross-slice calls are fully typed — no optional chaining needed.
const counterActs = createActs<MyState, MyActions>(function (get, set) {
  return {
    increment() {
      this.getState("count1"); // typed
    },
  };
});
import { createNexus, persist } from "nexus-state";

const nexus = createNexus({ state: { theme: "light", count: 0 } });

const stop = persist(nexus, {
  key: "my-app",
  include: ["theme"], // persist only the theme
  version: 1,
  migrate: (old) => ({ theme: old.theme ?? "light" }),
});

// later: stop() to disable persistence
// sessionStorage works through the same storage interface
persist(nexus, {
  key: "my-app-session",
  storage: sessionStorage,
});

For async loading (server, IndexedDB wrappers, files, etc.), load the data outside of persist and write it with context:

const profile = await loadProfile();
nexus.set({ profile }, { source: "server" });
— REACT —
// your-nexus-config
import { createReactNexus } from "nexus-state/react"; // import with /react

const nexus = createReactNexus({
  state: {
    count1: 0,
    count2: 0,
  },

  acts: (get, set) => ({
    increment() {
      set((state) => ({ count1: state.count1 + 1 }));
      this.getState("count1"); // ! calling another action
    },
    getState(value) {
      console.log(`${value}:`, get(value));
    },
  }),
});

export default nexus;
// The acts generic is optional — omitting it no longer causes an error.
const nexus = createReactNexus({ state: {...}, acts: (get, set) => ({...}) });

// Explicit form, if you prefer to declare shapes:
const typed = createReactNexus<MyState, MyActions>({...});
— CORE —
import nexus from "your-nexus-config";

const entireState = nexus.get();
const specificValue = nexus.get("key");
import nexus from "your-nexus-config";

nexus.set({ count1: 5, count2: 10 }); // partial, one notification
nexus.set((state) => ({ count1: state.count1 + 1 })); // functional

nexus.set({ user }, "server"); // provenance shortcut for { source: "server" }
nexus.set({ user }, { source: "server", meta: { requestId: 7 } });

✦ Sources: known values ("manual" | "storage" | "server" | "external" | "reset") autocomplete, but any string works. Sources starting with @@ are reserved for the library. ✦ Batching: one set with several keys notifies once — the primary way to batch. set calls made synchronously inside an action are collapsed into one too.

import nexus from "your-nexus-config";

nexus.reset(); // reset entire state
nexus.reset("count1", "count2"); // reset specific keys
import nexus from "your-nexus-config";

const unsubscribe = nexus.subscribe(
  (state, context) => {
    console.log("count1 changed:", state.count1, "from", context?.source);
  },
  ["count1"],
);

// A subscriber watching several keys is notified once per update, not per key.
nexus.subscribe((state) => save(state), ["count1", "count2"]);

// Watch every key:
nexus.subscribe((state) => save(state), ["*"]);

// later: unsubscribe() to disable subscribing
import nexus from "your-nexus-config";

const remove = nexus.middleware((prev, next, context) => {
  if (context?.source === "storage") {
    console.log("Loaded from storage:", next);
  }
  // Return a modified state, or nothing for a pure side effect.
  return next;
});

// later: remove() to detach middleware

Description: object containing your custom actions. Usage Example:

import nexus from "your-nexus-config";

nexus.acts.increment();
nexus.acts.getState("count1");

✦ Provenance: updates a set makes inside an action inherit the action name as their source (unless the set passes its own context), so subscribers, persist and devtools can tell which action changed the state. Nested this.other() calls keep the outer action's name.

Important: regular functions support calling other actions via this, arrow functions are more compact but don't:

// regular function
increment() {
  this.getState("count1"); // works
}

// arrow function
increment: () => this.getState("count1"); // not works

More info: Arrow Functions

— REACT —
import nexus from "your-nexus-config";

const entireState = nexus.use();
const specificValue = nexus.use("key");

✦ Note: Unlike get, use triggers a re-render when the watched state changes.

import nexus from "your-nexus-config";

// Subscribes to count1 and count2 only — inferred from the reads, no deps array:
const total = nexus.useSelector((s) => s.count1 + s.count2);

// New array/object each run (.map/.filter/literal) — pass "shallow" so an
// equal result doesn't re-render:
const ids = nexus.useSelector((s) => s.items.map((i) => i.id), "shallow");

// Escape hatch — custom comparator (e.g. arrays of objects):
const rows = nexus.useSelector(
  (s) => s.users.map((u) => ({ id: u.id, name: u.name })),
  (a, b) => a.length === b.length && a.every((x, i) => x.id === b[i].id),
);

✦ Note: read state through the selector argument, not nexus.get() — reads that bypass the argument aren't tracked. Comparison is on the selector's result: a tracked key changing re-runs the selector, but an equal result won't re-render. Selector/comparator are read from refs, so no useCallback is needed.

import nexus from "your-nexus-config";

const rerender = nexus.useRerender();
rerender(); // force re-render

Recipes

Integrations and patterns. DevTools ships as a tiny adapter; Immer and SSR are pure recipes over set / React context — nexus doesn't ship code it doesn't need.

import { createNexus } from "nexus-state";
import { devtools } from "nexus-state/devtools";

const nexus = createNexus({ state: { count: 0 } });
const stop = devtools(nexus, { name: "MyStore" });

nexus.set({ count: 1 }, "user"); // shows up as action "user"
// stop() disconnects
import { createNexus } from "nexus-state";
import { produce } from "immer";

const nexus = createNexus({
  state: {
    /* any shape, however nested */
  },
  acts: (get, set) => ({
    // one action can change anything on the draft:
    update() {
      set(
        produce(get(), (s) => {
          // s is your typed state — mutate whatever you need:
          // s.a.b.c = value;
          // s.list.push(item);
          // delete s.map[id];
        }),
      );
    },
  }),
});

// Outside an action it's the same one-liner:
nexus.set(
  produce(nexus.get(), (s) => {
    // mutate the draft
  }),
);

get() types the draft, so no generics are needed. Want a bound setter? It's a one-liner you own: const draft = (r) => nexus.set(produce(nexus.get(), r));

// store.ts — a factory, not a singleton
import { createReactNexus } from "nexus-state/react";

export function createStore(initial?: Partial<{ count: number }>) {
  return createReactNexus({
    state: { count: 0, ...initial },
    acts: (get, set) => ({
      increment() {
        set((s) => ({ count: s.count + 1 }));
      },
    }),
  });
}

export type Store = ReturnType<typeof createStore>;
// store-provider.tsx — standard React context (you own this ~10 lines)
"use client";
import { createContext, useContext, useRef, type ReactNode } from "react";
import { createStore, type Store } from "./store";

const StoreContext = createContext<Store | null>(null);

export function StoreProvider({
  initial,
  children,
}: {
  initial?: Partial<{ count: number }>;
  children: ReactNode;
}) {
  // create once per mount/request — never re-create on render
  const ref = useRef<Store>();
  if (!ref.current) ref.current = createStore(initial);
  return (
    <StoreContext.Provider value={ref.current}>
      {children}
    </StoreContext.Provider>
  );
}

export function useStore() {
  const store = useContext(StoreContext);
  if (!store) throw new Error("useStore must be used within <StoreProvider>");
  return store;
}
// pass server data as `initial` (matches the server-rendered HTML):
<StoreProvider initial={{ count: serverCount }}>
  <App />
</StoreProvider>;

// in a component — read via context, then use the store's hooks:
function Counter() {
  const store = useStore();
  const count = store.use("count");
  return <button onClick={store.acts.increment}>{count}</button>;
}

✦ With persist: localStorage only exists on the client, so persist is a no-op on the server. Initialize the store with server data for the first render (so it matches the server HTML), and call persist client-side after hydration (e.g. in a useEffect) so stored values don't cause a hydration mismatch.

License

MIT