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

@low-orbit/bruin

v1.2.3

Published

🐻 History-enabled state management for React, built on Zustand

Readme

npm version npm downloads License: MIT

A lightweight, performant state management library built on simplified flux patterns. Features a hook-based API that's straightforward and flexible, without unnecessary complexity. Every store automatically tracks history for undo/redo functionality.

While the bear mascot is friendly, the library is robust. Significant effort went into handling React's tricky edge cases, including the zombie child problem, react concurrency, and context loss between mixed renderers. Bruin handles these challenges correctly.

Built on Zustand for compatibility. If you're familiar with Zustand, you'll feel right at home with Bruin. The difference is automatic history tracking.

npm install @low-orbit/bruin

Creating a store

Stores are created as hooks. Store any data type: primitives, objects, or functions. Updates must be immutable, and the set function merges state automatically. History tracking happens automatically for every change.

import { create } from '@low-orbit/bruin'

interface BearState {
  bears: number
  increasePopulation: () => void
  removeAllBears: () => void
}

const useBearStore = create<BearState>((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 }),
}))

Use in your components

Use the store hook anywhere in your component tree—no providers required. Select the state you need and components re-render when those values change. Undo and redo are available directly on the store instance.

function BearCounter() {
  const bears = useBearStore((state) => state.bears)
  return <h1>{bears} around here ...</h1>
}

function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation)
  const { undo, redo, canUndo, canRedo } = useBearStore
  return (
    <>
      <button onClick={increasePopulation}>one up</button>
      <button onClick={undo} disabled={!canUndo}>undo</button>
      <button onClick={redo} disabled={!canRedo}>redo</button>
    </>
  )
}

Why choose Bruin over Redux?

Why choose Bruin over Context API?

  • Reduced boilerplate code
  • Selective re-renders based on state changes
  • Centralized state with action-based updates
  • Automatic history - Every change is tracked automatically

Why choose Bruin over Zustand?

  • Full Zustand compatibility, with additional features:
  • Automatic undo/redo - History built into every store
  • Transactions - Batch multiple updates together
  • History persistence - Save history across sessions with persistHistory

Use Cases

Bruin's history and snapshot functionality is ideal for:

  • Design tools - Image editors, UI builders, animation tools
  • Form builders - Dynamic forms, CMS panels, configuration editors
  • Data visualization - Analytics dashboards, chart builders, BI tools
  • Code editors - Online IDEs, query builders, API testing tools
  • Game development - Level editors, character creators, asset placement tools

→ See the History Example for headless components and UI integrations that demonstrate building history visualization UIs.


Recipes

Undo/redo history

Bruin adds automatic undo/redo history to every store:

interface BearState {
  bears: number
  increasePopulation: () => void
}

const useBearStore = create<BearState>((set) => ({
  bears: 0,
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
}))

// Undo/redo methods available on the store
useBearStore.undo() // Go back one step (restores entire state)
useBearStore.redo() // Go forward one step (restores entire state)
useBearStore.canUndo() // Check if undo is possible
useBearStore.canRedo() // Check if redo is possible

Important: Undo/redo restores the entire state to the previous snapshot (not just changed fields). Each store maintains its own independent history - calling undo() on one store doesn't affect others.

Named Snapshots

Save and restore specific state checkpoints by name:

interface CounterState {
  count: number
  increment: () => void
}

const useCounterStore = create<CounterState>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
}))

// Save a snapshot
const snapshotId = useCounterStore.getState().saveSnapshot('checkpoint-1')

// Make changes
useCounterStore.getState().increment()
useCounterStore.getState().increment()

// Restore the snapshot (adds to history)
useCounterStore.getState().loadSnapshot(snapshotId)

// List all snapshots
const snapshots = useCounterStore.getState().listSnapshots()
// [{ id: '...', name: 'checkpoint-1', timestamp: 1234567890 }]

// Get snapshot details
const snapshot = useCounterStore.getState().getSnapshot(snapshotId)

// Delete a snapshot
useCounterStore.getState().deleteSnapshot(snapshotId)

// Clear all snapshots
useCounterStore.getState().clearSnapshots()

Key points:

  • Snapshots are independent from history - they persist even if history is cleared
  • Loading a snapshot always adds a new history entry
  • Snapshots are deep-cloned to prevent reference sharing
  • Useful for saving "before experiment" states, templates, or checkpoints

Transactions

Transactions group multiple changes into a single history entry:

interface StoreState {
  count: number
  name: string
  initialize: () => void
}

// Option 1: Using set.transaction (recommended)
const useStore = create<StoreState>((set) => ({
  count: 0,
  name: '',
  initialize: () =>
    set.transaction(
      () => {
        set({ count: 1 })
        set({ name: 'John' })
      },
      { name: 'Initialize User' },
    ),
}))

// Option 2: Using store.transaction
useStore.transaction(
  () => {
    useStore.setState({ count: 1 })
    useStore.setState({ name: 'John' })
  },
  { name: 'Initialize User' },
)

Accessing all state

You can access the entire state object, but this will cause the component to re-render whenever any part of the state changes.

const state = useBearStore()

Selecting multiple state values

By default, Bruin uses strict equality (===) to detect changes, which works efficiently for primitive value selections.

const nuts = useBearStore((state) => state.nuts)
const honey = useBearStore((state) => state.honey)

When selecting multiple values into a single object (similar to Redux's mapStateToProps), use useShallow to prevent re-renders when the selected values haven't changed according to shallow equality.

import { create } from '@low-orbit/bruin'
import { useShallow } from '@low-orbit/bruin/react/shallow'

interface BearStore {
  nuts: number
  honey: number
  treats: Record<string, unknown>
}

const useBearStore = create<BearStore>((set) => ({
  nuts: 0,
  honey: 0,
  treats: {},
}))

// Object pick, re-renders the component when either state.nuts or state.honey change
const { nuts, honey } = useBearStore(
  useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
)

// Array pick, re-renders the component when either state.nuts or state.honey change
const [nuts, honey] = useBearStore(
  useShallow((state) => [state.nuts, state.honey]),
)

// Mapped picks, re-renders the component when state.treats changes in order, count or keys
const treats = useBearStore(useShallow((state) => Object.keys(state.treats)))

For advanced re-render control, you can provide a custom equality function (this requires using createWithEqualityFn).

const treats = useBearStore(
  (state) => state.treats,
  (oldTreats, newTreats) => compare(oldTreats, newTreats),
)

Replacing state entirely

The set function accepts a second parameter (defaults to false). When set to true, it replaces the entire state instead of merging. Take care not to accidentally remove important parts like action functions.

interface FishState {
  salmon: number
  tuna: number
  deleteEverything: () => void
  deleteTuna: () => void
}

const useFishStore = create<FishState>((set) => ({
  salmon: 1,
  tuna: 2,
  deleteEverything: () => set({}, true), // clears the entire store, actions included
  deleteTuna: () => set(({ tuna, ...rest }) => rest, true),
}))

Asynchronous actions

Bruin works seamlessly with async functions. Simply call set whenever your async operation completes.

interface FishState {
  fishies: Record<string, unknown>
  fetch: (pond: string) => Promise<void>
}

const useFishStore = create<FishState>((set) => ({
  fishies: {},
  fetch: async (pond: string) => {
    const response = await fetch(pond)
    set({ fishies: await response.json() })
  },
}))

Reading state within actions

While set supports function updates like set(state => result), you can also access the current state using get without triggering an update.

interface SoundState {
  sound: string
  action: () => void
}

const useSoundStore = create<SoundState>((set, get) => ({
  sound: 'grunt',
  action: () => {
    const sound = get().sound
    // ...
  },
}))

Accessing stores outside React components

When you need to read or modify state outside of React components, the store hook exposes utility methods directly.

:warning: This technique is not recommended for adding state in React Server Components (typically in Next.js 13 and above). It can lead to unexpected bugs and privacy issues for your users. For more details, see #2200.

interface DogState {
  paw: boolean
  snout: boolean
  fur: boolean
}

const useDogStore = create<DogState>(() => ({ paw: true, snout: true, fur: true }))

// Getting non-reactive fresh state
const paw = useDogStore.getState().paw
// Listening to all changes, fires synchronously on every change
const unsub1 = useDogStore.subscribe(console.log)
// Updating state, will trigger listeners
useDogStore.setState({ paw: false })
// Undo/redo available
useDogStore.undo()
useDogStore.redo()
// Unsubscribe listeners
unsub1()

// You can of course use the hook as you always would
function Component() {
  const paw = useDogStore((state) => state.paw)
  // ...
}

Subscribing to specific state slices

To subscribe only to specific parts of state, use the subscribeWithSelector middleware.

This middleware extends subscribe with an additional signature:

subscribe(selector, callback, options?: { equalityFn, fireImmediately }): Unsubscribe
import { subscribeWithSelector } from '@low-orbit/bruin/middleware'

interface DogState {
  paw: boolean
  snout: boolean
  fur: boolean
}

const useDogStore = create<DogState>(
  subscribeWithSelector(() => ({ paw: true, snout: true, fur: true })),
)

// Listening to selected changes, in this case when "paw" changes
const unsub2 = useDogStore.subscribe((state) => state.paw, console.log)
// Subscribe also exposes the previous value
const unsub3 = useDogStore.subscribe(
  (state) => state.paw,
  (paw, previousPaw) => console.log(paw, previousPaw),
)
// Subscribe also supports an optional equality function
const unsub4 = useDogStore.subscribe(
  (state) => [state.paw, state.fur],
  console.log,
  { equalityFn: shallow },
)
// Subscribe and fire immediately
const unsub5 = useDogStore.subscribe((state) => state.paw, console.log, {
  fireImmediately: true,
})

Using Bruin without React

Bruin's core functionality works without React. When using the vanilla version, createStore returns store utilities instead of a React hook.

import { createStore } from '@low-orbit/bruin/vanilla'

interface StoreState {
  count: number
  inc: () => void
}

const store = createStore<StoreState>((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}))

const { getState, setState, subscribe, undo, redo } = store

export default store

Vanilla stores can be used with the useStore hook (available since v4).

import { useStore } from '@low-orbit/bruin'
import { vanillaStore } from './vanillaStore'

const useBoundStore = <T,>(selector: (state: typeof vanillaStore extends { getState: () => infer S } ? S : never) => T) => 
  useStore(vanillaStore, selector)

:warning: Note that middlewares that modify set or get are not applied to getState and setState.

Transient updates for frequent state changes

For state that changes frequently, subscribe lets you listen without triggering re-renders. Combine with useEffect to automatically clean up subscriptions on unmount. This provides significant performance benefits when you can update the DOM directly.

import { useRef, useEffect } from 'react'

interface ScratchState {
  scratches: number
}

const useScratchStore = create<ScratchState>((set) => ({ scratches: 0 }))

const Component = () => {
  // Fetch initial state
  const scratchRef = useRef(useScratchStore.getState().scratches)
  // Connect to the store on mount, disconnect on unmount, catch state-changes in a reference
  useEffect(() => useScratchStore.subscribe(
    state => (scratchRef.current = state.scratches)
  ), [])
  // ...
}

Working with nested state? Try Immer

Updating deeply nested state structures can be tedious. Immer makes it much easier.

import { produce } from 'immer'

interface LushState {
  lush: {
    forest: {
      contains: { a: string } | null
    }
  }
  clearForest: () => void
}

const useLushStore = create<LushState>((set) => ({
  lush: { forest: { contains: { a: 'bear' } } },
  clearForest: () =>
    set(
      produce((state) => {
        state.lush.forest.contains = null
      }),
    ),
}))

const clearForest = useLushStore((state) => state.clearForest)
clearForest()

Alternatively, there are some other solutions.

Persist middleware

Save your store's state to any storage backend using the persist middleware.

import { create } from '@low-orbit/bruin'
import { persist, createJSONStorage } from '@low-orbit/bruin/middleware'

interface FishState {
  fishes: number
  addAFish: () => void
}

const useFishStore = create<FishState>()(
  persist(
    (set, get) => ({
      fishes: 0,
      addAFish: () => set({ fishes: get().fishes + 1 }),
    }),
    {
      name: 'food-storage', // name of the item in the storage (must be unique)
      storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
      persistHistory: true, // NEW: Persist undo/redo history across sessions
    },
  ),
)

See the full documentation for this middleware.

Immer middleware

Bruin includes Immer as a middleware option for easier nested state updates.

import { create } from '@low-orbit/bruin'
import { immer } from '@low-orbit/bruin/middleware/immer'

interface BeeState {
  bees: number
  addBees: (by: number) => void
}

const useBeeStore = create<BeeState>()(
  immer((set) => ({
    bees: 0,
    addBees: (by: number) =>
      set((state) => {
        state.bees += by
      }),
  })),
)

Prefer Redux-style reducers?

You can implement reducer patterns manually:

interface GrumpyState {
  grumpiness: number
  dispatch: (action: { type: 'INCREASE' | 'DECREASE'; by?: number }) => void
}

const types = { increase: 'INCREASE' as const, decrease: 'DECREASE' as const }

const reducer = (state: GrumpyState, action: { type: 'INCREASE' | 'DECREASE'; by?: number }) => {
  switch (action.type) {
    case types.increase:
      return { grumpiness: state.grumpiness + (action.by ?? 1) }
    case types.decrease:
      return { grumpiness: state.grumpiness - (action.by ?? 1) }
  }
}

const useGrumpyStore = create<GrumpyState>((set) => ({
  grumpiness: 0,
  dispatch: (args) => set((state) => reducer(state, args)),
}))

const dispatch = useGrumpyStore((state) => state.dispatch)
dispatch({ type: types.increase, by: 2 })

Alternatively, use the redux middleware which configures your reducer, sets initial state, and adds dispatch to both the state and vanilla API.

import { redux } from '@low-orbit/bruin/middleware'

const useGrumpyStore = create(redux(reducer, initialState))

Redux DevTools integration

Use the Redux DevTools Chrome extension with Bruin's devtools middleware.

import { devtools } from '@low-orbit/bruin/middleware'

interface PlainState {
  // your state here
}

// Usage with a plain action store, it will log actions as "setState"
const usePlainStore = create<PlainState>()(devtools((set) => ({ /* ... */ })))
// Usage with a redux store, it will log full action types
const useReduxStore = create(devtools(redux(reducer, initialState)))

Multiple stores with DevTools

Connect multiple stores to DevTools:

import { devtools } from '@low-orbit/bruin/middleware'

// Plain stores log actions as "setState"
const usePlainStore1 = create<PlainState>()(devtools((set) => ({ /* ... */ }), { name: 'Store1', store: 'storeName1' }))
const usePlainStore2 = create<PlainState>()(devtools((set) => ({ /* ... */ }), { name: 'Store2', store: 'storeName2' }))
// Redux stores log full action types
const useReduxStore1 = create(devtools(redux(reducer, initialState), { name: 'ReduxStore1', store: 'storeName3' }))
const useReduxStore2 = create(devtools(redux(reducer, initialState), { name: 'ReduxStore2', store: 'storeName4' }))

Different connection names separate stores in DevTools and allow grouping related stores together.

The devtools middleware accepts the store function as the first argument. The second argument can include a store name or serialize configuration.

Store naming: devtools(..., {name: "MyStore"}) creates a separate DevTools instance named "MyStore".

Serialization: devtools(..., { serialize: { options: true } }) configures serialization options.

Action Logging

Each store logs actions independently (unlike Redux's combined reducers). For combining stores, see https://github.com/pmndrs/zustand/issues/163

You can log a specific action type for each set function by passing a third parameter:

interface BearState {
  fishes: number
  eatFish: () => void
}

const useBearStore = create<BearState>()(devtools((set) => ({
  fishes: 0,
  eatFish: () => set(
    (prev) => ({ fishes: prev.fishes > 1 ? prev.fishes - 1 : 0 }),
    undefined,
    'bear/eatFish'
  ),
})))

You can also log the action's type along with its payload:

interface BearState {
  fishes: number
  addFishes: (count: number) => void
}

const useBearStore = create<BearState>()(devtools((set) => ({
  fishes: 0,
  addFishes: (count: number) => set(
    (prev) => ({ fishes: prev.fishes + count }),
    undefined,
    { type: 'bear/addFishes', count }
  ),
})))

If an action type is not provided, it is defaulted to "anonymous". You can customize this default value by providing an anonymousActionType parameter:

devtools(..., { anonymousActionType: 'unknown', ... })

If you wish to disable devtools (on production for instance). You can customize this setting by providing the enabled parameter:

devtools(..., { enabled: false, ... })

Using React Context

Stores created with create don't need context providers. However, you might want to use Context for dependency injection or to initialize stores with component props. Since stores are hooks, passing them directly as context values can violate React's rules of hooks.

The recommended approach (available since v4) is to use a vanilla store with Context.

import { createContext, useContext } from 'react'
import { createStore, useStore } from '@low-orbit/bruin'

interface StoreState {
  // your state here
}

const store = createStore<StoreState>((set) => ({ /* ... */ })) // vanilla store without hooks

const StoreContext = createContext<typeof store | null>(null)

const App = () => (
  <StoreContext.Provider value={store}>
    {/* ... */}
  </StoreContext.Provider>
)

const Component = () => {
  const store = useContext(StoreContext)
  if (!store) throw new Error('Store not found')
  const slice = useStore(store, (state) => state) // selector
  // ...
}

TypeScript Usage

Bruin is written in TypeScript and provides excellent type inference. All examples in this README use TypeScript.

import { create } from '@low-orbit/bruin'
import { devtools, persist } from '@low-orbit/bruin/middleware'
import type {} from '@redux-devtools/extension' // required for devtools typing

interface BearState {
  bears: number
  increase: (by: number) => void
}

const useBearStore = create<BearState>()(
  devtools(
    persist(
      (set) => ({
        bears: 0,
        increase: (by) => set((state) => ({ bears: state.bears + by })),
      }),
      {
        name: 'bear-storage',
      },
    ),
  ),
)

A more detailed TypeScript guide is here and there.

Third-Party Libraries

The community has created various extensions and integrations for Bruin. See third-party libraries documentation for available options.

Comparing with other libraries

How Bruin Extends Zustand

Bruin maintains full compatibility with Zustand while adding:

  • Automatic undo/redo - Every store tracks history automatically
  • Transactions - Batch multiple updates into single history entries
  • History persistence - Save history across sessions with persistHistory
  • Path subscriptions - Subscribe to specific nested object paths
  • Structural sharing - Efficient immutable updates via built-in Immer

All Zustand features and patterns work identically in Bruin.

License

MIT - Derivative work based on Zustand. See LICENSE for attribution.