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

zustand-store-addons

v1.0.0

Published

Computed properties, watchers, middleware chaining and more for Zustand stores

Readme

Zustand Store Addons

Computed properties, watchers, typed selectors, middleware chaining and automatic logs for Zustand stores.

Features

  • Computed properties — derived state that auto-updates when dependencies change
  • Watchers — callbacks triggered when specific state properties change
  • Typed selectors — string, array, and function selectors with full TypeScript inference
  • Middleware chaining — apply middleware via a flat array instead of nested wrapping
  • Automatic logging — configurable console logs for state changes
  • Zero extra dependencies — no lodash or other runtime libraries (only requires Zustand)
  • Full TypeScript support — JSDoc-documented API with rich IntelliSense for both TS and JS users
  • Zustand v4 & v5 compatible — works with zustand >=4.5.0

Installation

npm install zustand zustand-store-addons

[!NOTE] When using zustand v5, also install use-sync-external-store:

npm install use-sync-external-store

Quick Start

import create from 'zustand-store-addons';

interface MyStore {
  count: number;
  increment: () => void;
  above20: boolean;
  doubleCount: number;
  total: number;
}

const useStore = create<MyStore>(
  (set) => ({
    count: 0,
    increment: () => set(state => ({ count: state.count + 1 })),
    above20: false,
    doubleCount: 0,
    total: 0,
  }),
  {
    computed: {
      doubleCount(this: MyStore) { return this.count * 2; },
      total(this: MyStore) { return this.count + this.doubleCount; },
    },
    watchers: {
      total(newVal: number, oldVal: number) {
        if (newVal > 20 && oldVal <= 20) {
          this.set({ above20: true });
        }
      },
    },
    settings: { name: 'CounterStore', logLevel: 'diff' },
  }
);
function Counter() {
  // Array selector — full autocomplete per element
  const [count, doubleCount, total] = useStore(['count', 'doubleCount', 'total']);

  // Or single-key selector — full autocomplete + typed return
  const increment = useStore('increment');

  return (
    <div>
      <p>Count: {count}</p>
      <p>Count × 2: {doubleCount}</p>
      <p>Total: {total}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

Import

// Default import
import create from 'zustand-store-addons';

// Named import
import { createStore } from 'zustand-store-addons';

Addons Object

The second argument to createStore() is an optional addons object:

const useStore = create(
  (set, get) => ({ /* ...state... */ }),
  {
    computed: {},     // Computed properties
    watchers: {},     // Watcher callbacks
    middleware: [],   // Middleware chain
    settings: {},     // Store name & log level
  }
);

Computed Properties

Derived properties that auto-recalculate when their dependencies change. Use regular functions (not arrow functions) — this refers to the current state.

const useStore = create(
  (set) => ({
    count: 0,
    increment: () => set(state => ({ count: state.count + 1 })),
  }),
  {
    computed: {
      doubleCount(): number {
        return this.count * 2;
      },
      total(): number {
        return this.count + this.doubleCount;
      },
    },
  }
);

// State shape after computed merge:
// { count: 0, increment: fn, doubleCount: 0, total: 0 }

Typed Computed Properties

Computed properties can be fully typed by providing an explicit state interface to create() and typing the this context inside your computed methods:

interface CounterState {
  count: number;
  doubled: number;
}

const useStore = create<CounterState>(
  () => ({ count: 3, doubled: 0 }),
  {
    computed: {
      // Annotate `this` with your state interface
      doubled(this: CounterState) {
        return this.count * 2;
      },
    },
  }
);

const state = useStore.getState();
state.doubled  // ✅ typed as number
state.count    // ✅ typed as number

Typed Selectors

Three selector styles are available, all with TypeScript inference:

Function selector

Standard zustand pattern — full type inference:

const count = useStore(s => s.count);  // number

Single-key string selector

Full autocomplete, typed return value:

const count = useStore('count');  // number ✅

Array selector

Full per-element autocomplete, returns a typed tuple:

const [count, increment] = useStore(['count', 'increment']);
// [number, () => void] ✅

Comma-separated string selector

Returns a typed tuple via template literal parsing:

const [count, doubleCount, total] = useStore('count, doubleCount, total');
// [number, number, number] ✅

[!NOTE] IDE autocomplete is not available mid-string after commas. Use the array selector for the best autocomplete experience.


Watchers

Callbacks triggered when a specific state property changes. The method name must match the property to watch. Each callback receives (newValue, oldValue) as arguments.

Inside watchers, this provides { set, get, api } for reading and writing state.

const useStore = create(
  (set) => ({
    count: 0,
    above20: false,
    increment: () => set(state => ({ count: state.count + 1 })),
  }),
  {
    watchers: {
      count(newValue: number, oldValue: number) {
        console.log(`count: ${oldValue} → ${newValue}`);
        if (newValue > 20) {
          this.set({ above20: true });
        }
      },
    },
  }
);

Typed Watchers

Use the exported WatcherThis<TState> type for full typing of this inside watchers:

import type { WatcherThis } from 'zustand-store-addons';

interface MyState {
  count: number;
  above20: boolean;
}

// In the addons object:
watchers: {
  count(this: WatcherThis<MyState>, newVal: number, oldVal: number) {
    this.set({ above20: true });   // ✅ fully typed
    this.get().count;              // ✅ number
  },
}

Middleware Chaining

Apply middleware via a flat array — no more nested wrapping:

const logger = (config) => (set, get, api) =>
  config((args) => {
    console.log('applying', args);
    set(args);
    console.log('new state', get());
  }, get, api);

const useStore = create(
  (set) => ({ count: 0 }),
  {
    middleware: [logger],
  }
);

Logging

Configure logging via addons.settings:

{
  settings: {
    name: 'CounterStore',   // Shown in console group headers
    logLevel: 'diff'        // 'none' (default) | 'diff' | 'all'
  }
}

| Level | Output | |-------|--------| | 'none' | No logging | | 'diff' | Applied changes + computed updates | | 'all' | Previous state, changes, computed updates, and new state |

Excluding operations from logs

// Exclude frequently-updated properties from logs
set({ ticker: value }, { excludeFromLogs: true });

// Combine with state replacement
useStore.setState({ count: 0 }, { replace: true, excludeFromLogs: true });

Replacing state

// Replace entire state (instead of merging)
useStore.setState({ count: 0 }, true);

// Or using the settings object
useStore.setState({ count: 0 }, { replace: true });

API Reference

createStore(stateCreator, addons?)

| Parameter | Type | Description | |---|---|---| | stateCreator | (set, get, api) => State | State initializer (same as zustand's create()) | | addons.computed | Record<string, function> | Computed properties — this = current state | | addons.watchers | Record<string, function> | Watcher callbacks — this = { set, get, api } | | addons.middleware | Array<Function> | Middleware functions applied in order | | addons.settings | { name?, logLevel? } | Store name and log level |

Returned useStore hook

| Usage | Return Type | Description | |---|---|---| | useStore() | TState | Get entire state | | useStore(s => s.count) | Inferred | Function selector | | useStore('count') | TState['count'] | Single-key selector (full autocomplete) | | useStore(['a', 'b']) | [TState['a'], TState['b']] | Array selector (full autocomplete) | | useStore('a, b') | [TState['a'], TState['b']] | Comma-separated selector (typed tuple) | | useStore.getState() | TState | Read state outside React | | useStore.setState(partial) | void | Update state outside React | | useStore.subscribe(fn) | () => void | Subscribe to changes (returns unsubscribe) |

Exported Types

| Type | Description | |---|---| | SetState<T> | The set function signature | | SetStateAddons<T> | Extended set with SetStateSettings | | PartialState<T> | Partial<T> \| ((state: T) => Partial<T> \| void) | | SetStateSettings | { excludeFromLogs?, replace? } | | AddonsSettings | { name?, logLevel? } | | Addons | Full addons configuration object | | UseStore<T> | The returned hook/API interface | | WatcherThis<T> | this context inside watchers ({ set, get, api }) | | ComputedDef<T> | Typed map of computed getter functions | | InferComputed<T> | Extracts computed return types into a plain object type | | LogLevel | Enum: None, Diff, All |


What's New in v1.0.0

  • Zustand v5 support — compatible with both v4 (4.5+) and v5 via createWithEqualityFn
  • Zero extra dependencies — removed lodash-es, string.prototype.matchall, and all other extra runtime deps
  • Typed computed properties — properly type derived state by passing explicit interfaces and binding this context
  • Typed selectors — single-key, array, and comma-separated selectors with full TypeScript inference
  • JSDoc documentation — rich IntelliSense tooltips for all exported types and functions
  • Immer middleware supportPartialState<T> now accepts void returns for immer-style mutations
  • Modern build tooling — dual ESM/CJS output via tsup with TypeScript declaration files
  • Smaller bundle — ~8KB (down from ~25KB with lodash)

Compatibility

| Dependency | Version | |---|---| | zustand | >=4.5.0 | | react | >=17 (optional) | | use-sync-external-store | >=1.2.0 (optional, required for zustand v5) |

License

MIT