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

@zap-studio/store

v1.0.0

Published

Small, framework-agnostic state container with auto-tracked derived values and optional built-in persist.

Readme

@zap-studio/store

A small state container. It works with any framework, or no framework at all. It has auto-tracked derived values and simple built-in persist.

Full documentation: zapstudio.dev/store

Motivation

Zustand's setState does a shallow merge by default. This is a problem for nested state. It also has no built-in derived value: every selector runs again from scratch, with no caching.

Jotai's atom model is nice, but jotai/utils alone has more than a dozen helper atoms. One simple idea ("a derived value") grew into a large API.

TanStack Store has the best reactive core of the three. It uses an auto-tracked signal graph, so derived values only recompute when a real dependency changes. But subscribe() returns an object shaped like RxJS's Subscription, not a plain function. It also has no built-in persistence.

@zap-studio/store takes TanStack's auto-tracked reactivity and Zustand's simple, single-factory style. The public API stays small on purpose: createStore, derive, and nothing else. No middleware chain, no Provider, no atom zoo.

Installation

npm install @zap-studio/store

Features

  • One factory per store, via createStore(initialState, actionsFactory?, options?). Actions are created once, not on every render or every consumer.
  • get() is stable — it returns the same reference across calls, until the state actually changes. Safe to use as a snapshot source for things like React's useSyncExternalStore.
  • set takes an updater only: set((prev) => partialOrFullState). The result is always shallow-merged. There is no set({ ... }) shortcut, so there is no confusion between "merge" and "replace".
  • Auto-tracked derived values, via derive(deps, fn). The value is cached. It only recomputes when something fn actually read last time has changed. fn can read from any store, even one not listed in deps, and it will still track correctly. deps only sets the order and the types of the arguments.
  • Plain unsubscribe functionssubscribe(...) returns () => void, not a Subscription object.
  • Simple built-in persist — pass { persist: { key, storage } } to createStore. storage only needs getItem, setItem, and removeItem, so localStorage and sessionStorage work as-is.
  • No required runtime dependencies.
  • Full TypeScript inference for state, actions, and the values passed into derive. You do not need to write generic types by hand.

Quick Start

import { createStore } from "@zap-studio/store";

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

counter.get(); // { count: 0, increment: fn }
counter.getState(); // { count: 0 }

const unsubscribe = counter.subscribe((state) => console.log(state));
counter.get().increment(); // logs { count: 1, increment: fn }

set

set only takes an updater function: set((prev) => partialOrFullState). The result is always shallow-merged into the state.

const user = createStore({ name: "Ada", age: 30 }, (set) => ({
  haveBirthday: () => set((s) => ({ age: s.age + 1 })), // only `age` changes
}));

derive

An auto-tracked, cached derived value:

import { derive } from "@zap-studio/store";

const double = derive([counter], (s) => s.count * 2);

double.get();
const unsubscribe = double.subscribe((value) => console.log(value));

derive can also depend on another derive value, so you can build derived values from derived values:

const quadruple = derive([double], (v) => v * 2);

Dependency tracking is automatic. The real dependencies are whatever fn reads the last time it runs — not just what is in the deps array. deps exists to give you readable, positionally-typed arguments. You can pass [] and read stores directly inside fn instead:

const double = derive([], () => counter.getState().count * 2);

A derived value only tells its subscribers about a change when the computed value is actually different (checked with Object.is). This is true even if a dependency's state changed underneath it.

Persist

Persist is simple, and built into createStore — it is not a separate package:

const counter = createStore(
  { count: 0 },
  (set) => ({ increment: () => set((s) => ({ count: s.count + 1 })) }),
  { persist: { key: "counter", storage: localStorage } },
);
  • storage only needs getItem, setItem, and removeItem — the standard Storage shape. So localStorage and sessionStorage work with no extra code.
  • There is no version or migration system. If the stored value is corrupt or does not match, it is ignored, and initialState is used instead.
  • Only plain state is saved. Actions (functions) are never saved.

Runtime Support

| Runtime | Minimum version | | ------------------ | --------------------------------------- | | Node.js | 18.0.0 | | Bun | 1.0.0 | | Deno | 1.42 | | Cloudflare Workers | Any current release | | Browsers | Chrome/Edge 98, Firefox 97, Safari 15.4 |

License

MIT