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

@bkincz/clutch

v1.3.0

Published

A production-ready, TypeScript-first state management library built on Immer with advanced features

Readme

Clutch

Release codecov npm version License: MIT

A TypeScript-first state manager built on Immer with undo/redo, persistence, and debugging tools.

Installation

pnpm add @bkincz/clutch
npm install @bkincz/clutch
yarn add @bkincz/clutch

Quick Start

import { StateMachine } from '@bkincz/clutch'

interface AppState {
  count: number
  todos: string[]
}

const state = new StateMachine({
  initialState: { count: 0, todos: [] }
})

// Mutate state with simple, mutable-style code
state.mutate(draft => {
  draft.count++
  draft.todos.push('Learn Clutch')
})

// Undo/Redo out of the box
state.undo()
state.redo()

Core Features

Immutable Updates

Powered by Immer - write simple mutations, get immutable state.

// Instead of this
const newState = {
  ...state,
  todos: state.todos.map(todo =>
    todo.id === id ? { ...todo, completed: true } : todo
  )
}

// Write this
state.mutate(draft => {
  const todo = draft.todos.find(t => t.id === id)
  if (todo) todo.completed = true
})

Undo/Redo

Built-in history management using efficient patch-based storage.

state.mutate(draft => { draft.count++ }, 'increment')
state.mutate(draft => { draft.count++ }, 'increment')

state.undo() // count is back to 1
state.redo() // count is 2 again

state.clearHistory() // start fresh

Batch Operations

Group multiple changes into a single undo/redo step.

state.batch([
  draft => { draft.count++ },
  draft => { draft.todos.push('New todo') },
  draft => { draft.loading = false }
], 'bulk update')

Persistence

Automatic localStorage backup with optional server sync.

const state = new StateMachine({
  initialState: { count: 0 },
  persistenceKey: 'my-app',
  autoSaveInterval: 5 // minutes
})

// Optional: add server persistence
class MyState extends StateMachine<AppState> {
  protected async saveToServer(state: AppState): Promise<void> {
    await fetch('/api/state', {
      method: 'POST',
      body: JSON.stringify(state)
    })
  }

  protected async loadFromServer(): Promise<AppState | null> {
    const res = await fetch('/api/state')
    return res.ok ? res.json() : null
  }
}

Advanced Features

Middleware

Intercept mutations for validation, logging, or transformation.

import { Middleware } from '@bkincz/clutch'

// Validation middleware
const validateCount: Middleware<AppState> = (ctx, next, draft) => {
  next(draft)
  if (draft.count < 0) {
    throw new Error('Count cannot be negative')
  }
}

// Logging middleware
const logger: Middleware<AppState> = (ctx, next, draft) => {
  console.log('Before:', ctx.state)
  next(draft)
  console.log('After:', draft)
}

const state = new StateMachine({
  initialState: { count: 0 },
  middleware: [validateCount, logger]
})

Middleware executes in order, like Express.js:

  1. First middleware runs "before" code
  2. Calls next(draft) to pass control to next middleware
  3. After all middleware, the mutation executes
  4. Control returns back through middleware "after" code

Selective Persistence

Exclude sensitive fields from localStorage.

interface AppState {
  user: { name: string; email: string }
  authToken: string
  preferences: object
}

const state = new StateMachine({
  initialState: { ... },
  persistenceKey: 'my-app',

  // Option 1: Exclude specific fields
  persistenceFilter: {
    exclude: ['authToken']
  },

  // Option 2: Include only specific fields
  persistenceFilter: {
    include: ['user', 'preferences']
  },

  // Option 3: Custom filter function
  persistenceFilter: {
    custom: (state) => ({
      user: { name: state.user.name }, // exclude email
      preferences: state.preferences
    })
  }
})

Excluded fields automatically fall back to initialState when loaded from localStorage.

DevTools Integration

Connect to Redux DevTools browser extension for time-travel debugging.

const state = new StateMachine({
  initialState: { count: 0 },

  // Simple: enable with defaults
  enableDevTools: true,

  // Advanced: customize behavior
  enableDevTools: {
    name: 'MyApp',           // Name in DevTools
    maxAge: 50,              // Max actions to keep
    latency: 500,            // Debounce updates
    features: {
      jump: true,            // Enable time-travel
      skip: false,
      export: true,
      import: false
    }
  }
})

// Now open Redux DevTools extension to see:
// - All mutations with descriptions
// - State at each step
// - Time-travel through history
// - Import/export state

Gracefully degrades when DevTools extension is not installed.

StateRegistry (Multi-Machine Management)

Consolidate multiple state machines into a single coordinated store.

import { StateMachine, StateRegistry } from '@bkincz/clutch'

// Define your state types
type UserState = { name: string; email: string }
type TodosState = { items: { id: string; text: string }[] }

// Define the store's machine registry
type AppMachines = {
  user: UserState
  todos: TodosState
}

// Create individual machines
class UserMachine extends StateMachine<UserState> {
  constructor() {
    super({ initialState: { name: '', email: '' } })
  }
}

class TodosMachine extends StateMachine<TodosState> {
  constructor() {
    super({ initialState: { items: [] } })
  }
}

// Create store and register machines
const store = new StateRegistry<AppMachines>()
store.register('user', new UserMachine())
store.register('todos', new TodosMachine())

// Get combined state from all machines
const state = store.getState()
// { user: { name: '', email: '' }, todos: { items: [] } }

// Subscribe to any machine's changes
store.subscribe((combinedState) => {
  console.log('Something changed:', combinedState)
})

// Coordinated operations across all machines
store.resetAll()        // Reset all machines to initial state
store.clearAllHistory() // Clear undo history on all machines
store.forceSaveAll()    // Persist all machines
store.destroyAll()      // Clean up everything

Note: When defining your machine registry type, use type instead of interface for TypeScript compatibility.

Multi-Instance Sync

Sync state across browser tabs using BroadcastChannel.

const state = new StateMachine({
  initialState: { count: 0 },

  // Simple: enable with defaults
  enableSync: true,

  // Advanced: customize behavior
  enableSync: {
    channel: 'my-app-sync',      // BroadcastChannel name
    syncDebounce: 50,             // Debounce updates (ms)
    mergeStrategy: 'patches'      // 'patches' or 'latest'
  }
})

// Now changes in one tab instantly appear in all other tabs
// - 'patches': Send only the changes (more efficient)
// - 'latest': Send full state (simpler, more reliable)

Works automatically in the background. Gracefully degrades when BroadcastChannel is not supported.

Lifecycle Events

Subscribe to state changes, errors, and cleanup.

// Subscribe to mutations
const unsubscribe = state.on('afterMutate', (payload) => {
  console.log(`[${payload.operation}] ${payload.description}`)
  console.log('Patches:', payload.patches)
  console.log('New state:', payload.state)
})

// Subscribe to errors
state.on('error', (payload) => {
  console.error(`Error in ${payload.operation}:`, payload.error)
})

// Subscribe to cleanup
state.on('destroy', (payload) => {
  console.log('Final state:', payload.finalState)
})

// Cleanup when done
unsubscribe()

Available Events:

  • afterMutate - After any successful mutation (mutate, batch, undo, redo)
  • error - When a mutation or persistence operation fails
  • destroy - Before the state machine is cleaned up

React Hooks

Note: React hooks are imported from @bkincz/clutch/react.

useStateMachine(state)

Subscribe to entire state.

import { useStateMachine } from '@bkincz/clutch/react'

function Counter() {
  const { state, mutate } = useStateMachine(todoState)

  return (
    <button onClick={() => mutate(draft => { draft.count++ })}>
      Count: {state.count}
    </button>
  )
}

useStateSlice(state, selector)

Subscribe to a slice for better performance.

const todoCount = useStateSlice(state, s => s.todos.length)
const completedTodos = useStateSlice(state, s => s.todos.filter(t => t.completed))

useStateActions(state)

Get mutation methods without subscribing.

const { mutate, batch, undo, redo } = useStateActions(state)

useStateHistory(state)

Access undo/redo controls.

const { canUndo, canRedo, undo, redo } = useStateHistory(state)

useStatePersist(state)

Handle persistence operations.

const { save, load, isSaving, hasUnsavedChanges } = useStatePersist(state)

useLifecycleEvent(state, event, listener)

Subscribe to lifecycle events with automatic cleanup.

useLifecycleEvent(state, 'afterMutate', (payload) => {
  console.log('State changed:', payload.state)
})

createStateMachineHooks(state)

Create pre-bound hooks for convenience.

import { createStateMachineHooks } from '@bkincz/clutch/react'

const hooks = createStateMachineHooks(todoState)

function TodoApp() {
  const { state, mutate } = hooks.useState()
  const { canUndo, undo } = hooks.useHistory()

  hooks.useLifecycle('afterMutate', (payload) => {
    console.log('Changed:', payload.description)
  })

  return <div>...</div>
}

useRegistry(store)

Subscribe to combined state from a StateRegistry.

const state = useRegistry(store)
// { user: { name: '', email: '' }, todos: { items: [] } }

useRegistrySlice(store, selector)

Subscribe to a slice of combined state for better performance.

const userName = useRegistrySlice(store, s => s.user.name)
const todoCount = useRegistrySlice(store, s => s.todos.items.length)

useRegistryMachine(store, machineName)

Subscribe to a specific machine's state.

const userState = useRegistryMachine(store, 'user')
const todosState = useRegistryMachine(store, 'todos')

useRegistryActions(store)

Get registry-wide actions without subscribing.

const { resetAll, forceSaveAll, clearAllHistory, destroyAll } = useRegistryActions(store)

createRegistryHooks(store)

Create pre-bound hooks for a specific StateRegistry.

import { createRegistryHooks } from '@bkincz/clutch/react'

const hooks = createRegistryHooks(store)

function App() {
  const state = hooks.useRegistry()
  const userState = hooks.useMachine('user')
  const { resetAll } = hooks.useActions()

  return <div>...</div>
}

Configuration

interface StateConfig<T> {
  // Required
  initialState: T

  // Persistence
  persistenceKey?: string              // localStorage key
  persistenceFilter?: PersistenceFilter<T> // exclude/include/custom
  enablePersistence?: boolean          // default: true
  autoSaveInterval?: number            // minutes, default: 5
  enableAutoSave?: boolean             // default: true

  // History
  maxHistorySize?: number              // default: 50

  // Middleware
  middleware?: Middleware<T>[]

  // DevTools
  enableDevTools?: boolean | DevToolsConfig

  // Sync
  enableSync?: boolean | SyncConfig

  // Validation & Debugging
  validateState?: (state: T) => boolean
  enableLogging?: boolean              // default: false
}

API Reference

Core Methods

getState(): T                          // Get current state
mutate(recipe, description?)           // Update state
batch(mutations, description?)         // Batch multiple mutations
subscribe(listener)                    // Subscribe to changes
undo(): boolean                        // Undo last operation
redo(): boolean                        // Redo next operation
destroy()                              // Clean up resources

Lifecycle Methods

on(event, listener): () => void        // Subscribe to events

Persistence Methods

forceSave(): Promise<void>             // Immediately save
hasUnsavedChanges(): boolean           // Check unsaved changes
loadFromServerManually(): Promise<boolean> // Manual server load

History Methods

getHistoryInfo(): StateHistoryInfo    // Get history state
clearHistory(): void                   // Clear undo/redo
canUndo(): boolean                     // Check if undo available
canRedo(): boolean                     // Check if redo available

Reset Methods

reset(): void                          // Reset to initial state
getInitialState(): T                   // Get the initial state

StateRegistry Methods

register(name, machine)                // Register a machine
unregister(name)                       // Remove a machine
getMachine(name)                       // Get a registered machine
getMachineNames()                      // List all machine names
getState()                             // Get combined state
getMachineState(name)                  // Get specific machine state
subscribe(listener)                    // Subscribe to any change
subscribeToMachine(name, listener)     // Subscribe to specific machine
resetAll()                             // Reset all machines
forceSaveAll()                         // Save all machines
clearAllHistory()                      // Clear all history
destroyAll()                           // Destroy all machines

Performance

  • Lightweight: ~20KB minified
  • Fast mutations: < 1ms average overhead
  • Efficient undo/redo: Patch-based storage
  • Optimized rendering: Fine-grained subscriptions
  • Lazy initialization: Zero-cost for unused features
  • Tree-shakeable: Only bundle what you use

TypeScript

Fully typed with automatic inference.

const state = new StateMachine({
  initialState: { count: 0, name: 'John' }
})

// TypeScript knows the exact shape
state.mutate(draft => {
  draft.count++      // ✓ number
  draft.name = 'Jane' // ✓ string
  draft.age = 25     // ✗ Property 'age' does not exist
})

License

MIT