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 🙏

© 2025 – Pkg Stats / Ryan Hefner

zust

v1.0.6

Published

A powerful, lightweight, and fully standalone state management library for React with time-travel debugging, computed values, and zero dependencies

Readme

Zust State Management

npm version License: MIT All Contributors

A powerful, lightweight, and fully standalone state management library for React applications.

Zust provides an intuitive API for managing complex application state with advanced features like time-travel debugging, computed values, async operations, and more - all with zero dependencies (except React).

✨ Features

  • 🎯 Zero Dependencies - Fully standalone, no external dependencies
  • 🚀 Lightweight - Minimal bundle size with maximum performance
  • 🔥 Type-Safe - Full TypeScript support with excellent type inference
  • 🎨 Intuitive API - Simple dot-notation paths for nested state updates
  • 📦 Array Support - Native support for array indices in paths ("todos.0.done")
  • ⏱️ Time-Travel - Built-in undo/redo with history management
  • 🧮 Computed Values - MobX-style cached computed properties
  • Async Actions - First-class async/await support with dispatch
  • 🔔 Granular Subscriptions - Subscribe to specific paths for optimal performance
  • 🎛️ Batched Updates - Automatic batching to minimize re-renders
  • 💾 Persistence - Built-in localStorage/sessionStorage support
  • 🔒 Secure - Protection against prototype pollution attacks
  • 🧩 Extensible - Middleware and plugin system for customization

Live Example

Check out the interactive example app in the example/ folder to see all features in action. To run it:

cd example
npm install  # or bun install
npm run dev  # or bun dev

Then open http://localhost:3000 to see the interactive demo.

Table of Contents

Installation

npm install zust

or

bun install zust

Quick Start

import { createStore } from 'zust';

// Define your initial state
const initialState = {
  user: { name: 'John', age: 30 },
  todos: [
    { id: 1, text: 'Learn Zust', done: false },
    { id: 2, text: 'Build app', done: false }
  ],
  settings: { theme: 'light' as 'light' | 'dark' },
};

// Create the store
const { useSelectors, setDeep, getState } = createStore(initialState);

function App() {
  // Select multiple values efficiently
  const { name, theme } = useSelectors('user.name', 'settings.theme');

  // Update nested state with ease
  const updateName = () => setDeep('user.name', 'Jane');
  const toggleTheme = () => setDeep('settings.theme',
    prev => prev === 'light' ? 'dark' : 'light'
  );

  return (
    <div>
      <p>User: {name}</p>
      <p>Theme: {theme}</p>
      <button onClick={updateName}>Update Name</button>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

Core Concepts

Basic Usage

Zust uses dot-notation paths to access and update deeply nested state:

const { setDeep, getState } = createStore({
  user: {
    profile: {
      name: 'John',
      email: '[email protected]'
    },
    preferences: {
      notifications: true
    }
  }
});

// Update nested values
setDeep('user.profile.name', 'Jane');
setDeep('user.preferences.notifications', false);

// Use functional updates
setDeep('user.profile.name', prevName => prevName.toUpperCase());

// Access current state
const currentState = getState();
console.log(currentState.user.profile.name); // 'JANE'

Array Paths

Zust has native support for array indices in paths:

const { setDeep } = createStore({
  todos: [
    { id: 1, text: 'Task 1', done: false },
    { id: 2, text: 'Task 2', done: false }
  ]
});

// Update array items using index notation
setDeep('todos.0.done', true);
setDeep('todos.1.text', 'Updated Task 2');

// Works with nested arrays
setDeep('matrix.0.1.value', 42);

Async Operations

Dispatch async actions with first-class async/await support:

const store = createStore({
  data: null,
  loading: false,
  error: null
});

const state = store.getState();

// Dispatch async actions
await state.dispatch(async (state, setDeep) => {
  setDeep('loading', true);

  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    setDeep('data', data);
  } catch (error) {
    setDeep('error', error.message);
  } finally {
    setDeep('loading', false);
  }
});

Advanced Features

Time-Travel Debugging

Enable undo/redo functionality with built-in history management:

const { getState, history } = createStore(
  { counter: 0 },
  {
    history: {
      enabled: true,
      maxSize: 50,        // Maximum history entries (default: 50)
      debounceMs: 100     // Debounce captures (default: 100ms)
    }
  }
);

const state = getState();

// Make some changes
setDeep('counter', 1);
setDeep('counter', 2);
setDeep('counter', 3);

// Undo/redo
if (history?.canUndo()) {
  history.undo();  // counter is now 2
}

if (history?.canRedo()) {
  history.redo();  // counter is back to 3
}

// Jump to specific state
history?.jump(-2);  // Go back 2 states

// Clear history
history?.clear();

Computed Values

Define cached computed properties that automatically recompute when dependencies change:

const { getState } = createStore(
  {
    firstName: 'John',
    lastName: 'Doe',
    items: [{ price: 10 }, { price: 20 }]
  },
  {
    computedValues: {
      // Simple computed value
      fullName: (state) => `${state.firstName} ${state.lastName}`,

      // Computed value with explicit dependencies
      total: {
        compute: (state) => state.items.reduce((sum, item) => sum + item.price, 0),
        deps: ['items'],  // Only recompute when items change
        cache: true       // Cache the result (default: true)
      }
    }
  }
);

const state = getState();
console.log(state.fullName);  // 'John Doe'
console.log(state.total);      // 30

// Computed values update automatically
setDeep('firstName', 'Jane');
console.log(getState().fullName);  // 'Jane Doe'

Path-Based Subscriptions

Subscribe to changes on specific paths for optimal performance:

const { subscribePath } = createStore({
  user: { name: 'John', age: 30 },
  settings: { theme: 'light' }
});

// Subscribe to specific path
const unsubscribe = subscribePath('user.name', (newValue, oldValue, fullState) => {
  console.log(`Name changed from ${oldValue} to ${newValue}`);
});

setDeep('user.name', 'Jane');  // Triggers callback
setDeep('user.age', 31);        // Does NOT trigger callback

// Unsubscribe when done
unsubscribe();

Batched Updates

Batch multiple updates to minimize re-renders:

import { batch } from 'zust';

const { setDeep, subscribe } = createStore({ a: 0, b: 0, c: 0 });

let renderCount = 0;
subscribe(() => renderCount++);

// Without batching: 3 renders
setDeep('a', 1);
setDeep('b', 2);
setDeep('c', 3);

// With batching: 1 render
batch(() => {
  setDeep('a', 1);
  setDeep('b', 2);
  setDeep('c', 3);
});

Persistence

Persist state to localStorage or sessionStorage:

import { createStore, createPersistConfig } from 'zust';

const { useSelectors, setDeep } = createStore(
  {
    user: { name: 'John', age: 30 },
    settings: { theme: 'light', language: 'en' }
  },
  {
    persist: createPersistConfig('user', 'settings.theme'),
    prefix: 'myapp'  // localStorage key prefix
  }
);

// Persist entire store
const store2 = createStore(initialState, { persist: true });

API Reference

createStore<T>(initialState, options?)

Creates a Zust store with the provided initial state and options.

Parameters:

  • initialState: T - The initial state object (must be non-null object)
  • options?: StoreOptions<T> - Configuration options

Returns: StoreCreationResult<T> containing:

  • useStore() - React hook that returns the enhanced store
  • useSelectors(...paths) - Hook to select multiple state values
  • getState() - Get current state with methods (dispatch, setDeep, etc.)
  • setState(partial, replace?) - Set state (shallow or deep merge)
  • setDeep(path, value) - Update nested state by path
  • subscribe(listener) - Subscribe to all state changes
  • subscribePath(path, callback) - Subscribe to specific path changes
  • destroy() - Cleanup and destroy the store
  • history? - History API (if history is enabled)

StoreOptions<T>

  • persist?: boolean | PersistConfig<T> - Enable state persistence
  • prefix?: string - Prefix for localStorage keys
  • logging?: boolean - Enable console logging
  • middleware?: Middleware<T>[] - Array of middleware functions
  • computedValues?: ComputedValues<T> - Computed properties definition
  • plugins?: Plugin<T>[] - Store plugins
  • history?: HistoryConfig - Time-travel debugging configuration

HistoryConfig

  • enabled: boolean - Enable history tracking
  • maxSize?: number - Maximum history entries (default: 50)
  • debounceMs?: number - Debounce delay for captures (default: 100ms)

batch(fn: () => void)

Batch multiple state updates into a single notification.

Enhanced Store Methods

The store returned by getState() or useStore() includes:

  • setDeep(path, action) - Update state by path
  • dispatch(asyncAction) - Execute async actions
  • subscribe(listener) - Subscribe to state changes
  • subscribePath(path, callback) - Subscribe to path changes
  • deleteDeep(path) - Delete property by path
  • hasPath(path) - Check if path exists
  • history? - Undo/redo API (if enabled)

Migration Guide

From Version 0.x

Version 1.0 is a complete rewrite with breaking changes:

What's New:

  • ✅ Fully standalone (no Zustand dependency)
  • ✅ Array path support
  • ✅ Time-travel debugging
  • ✅ Async dispatch
  • ✅ Computed values with caching
  • ✅ Path-based subscriptions
  • ✅ Better TypeScript types

Breaking Changes:

  • Removed Zustand middleware compatibility
  • getState() now returns enhanced store (not raw state)
  • Internal engine completely rewritten

Migration Steps:

  1. Update imports (API is mostly backward compatible)
  2. Remove any Zustand-specific middleware
  3. Update TypeScript types if using advanced features
  4. Test your application thoroughly

Tests

Zust includes comprehensive tests:

  • All 69 tests passing
  • Integration tests with React
  • Unit tests for all features
  • Security tests (prototype pollution protection)
  • Edge case handling

Run tests:

npm test

License

MIT License. See the LICENSE file for details.

Contributors ✨

Thanks goes to these wonderful people (emoji key):

This project follows the all-contributors specification. Contributions of any kind welcome!