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

use-undoable-next

v5.1.3

Published

React hook for undo/redo functionality with customizable mutation behaviors and zero dependencies.

Readme

use-undoable-next

Undo/redo for React, without the hassle.

npm version npm downloads license react peer dep types bundle size

A React hook that adds undo/redo functionality to useState with a familiar API, customizable mutation behaviors, and zero dependencies.

Topics: react · react-hook · undo · redo · history · state-management · typescript


Features

| Feature | Description | |---------|-------------| | Familiar API | Mirrors useState; drop-in replacement with undo/redo support | | 4 mutation behaviors | mergePastReversed, mergePast, destroyFuture, keepFuture | | Per-call overrides | Change behavior on-the-fly for individual setState calls | | History limit | Cap memory usage with a configurable historyLimit | | Functional updaters | setState(prev => prev + 1) works just like useState | | Zero dependencies | No bloat; tiny footprint, ships as ESM + CJS | | TypeScript native | Full type safety with generic state inference |


Benchmark vs use-undoable

use-undoable-next is a modernized fork of use-undoable with a smaller footprint and faster reducer logic.

Bundle size

| Metric | use-undoable 5.0.0 | use-undoable-next 5.1.2 | Improvement | |--------|---------------------|---------------------------|-------------| | ESM bundle | 6.86 KB | 5.49 KB | 20% smaller | | CJS bundle | 6.88 KB | 6.50 KB | 5% smaller | | npm tarball | 14.96 KB | 11.89 KB | 21% smaller | | Unpacked size | 66.59 KB (13 files) | ~28 KB (9 files) | 58% smaller | | Runtime deps | 0 | 0 | — |

Reducer speed

Benchmark: 50,000 iterations, each performing 50 updates + 25 undos + 25 redos (100 ops/iteration = 5M total ops). Measured on Node 26, Windows.

| Package | Total time | Per op | Ops/sec | Relative | |---------|-----------|--------|---------|----------| | use-undoable 5.0.0 | ~1,447 ms | 0.029 ms | ~34,700 | 1.00x (baseline) | | use-undoable-next 5.1.2 | ~781 ms | 0.016 ms | ~64,300 | 1.85x faster |

The speedup comes from leaner reducer logic (arrow functions, modern spread syntax, fewer intermediate variables) and a streamlined mutate path.


Migrating from use-undoable

The API is 100% compatible — only the package name changes. No code logic changes required.

Imports

- import useUndoable from "use-undoable";
+ import useUndoable from "use-undoable-next";

Type imports (if used)

- import type { Options, MutationBehavior } from "use-undoable";
+ import type { Options, MutationBehavior } from "use-undoable-next";

Everything else stays the same

The hook signature, return tuple, options, behaviors, and helpers are identical:

// ✅ No changes needed — this works as-is
const [state, setState, { undo, redo, canUndo, canRedo, reset, past, future }] =
  useUndoable(initialState, {
    behavior: "mergePastReversed",
    historyLimit: 100,
    ignoreIdenticalMutations: true,
  });

Installation

npm install use-undoable-next
yarn add use-undoable-next
pnpm add use-undoable-next

Quick Start

import useUndoable from "use-undoable-next"

function Counter() {
  const [count, setCount, { undo, redo, canUndo, canRedo }] = useUndoable(0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <button onClick={() => setCount(c => c - 1)}>−</button>
      <button onClick={undo} disabled={!canUndo}>↶ Undo</button>
      <button onClick={redo} disabled={!canRedo}>↷ Redo</button>
    </div>
  )
}

How It Works

useUndoable maintains three pieces of state internally: a past stack, the present value, and a future stack. Every setState pushes the current value into past. Calling undo moves the present into future and pops the last item from past into present. redo does the reverse.

---
title: Undo/Redo State Flow
---
stateDiagram-v2
    [*] --> Idle

    Idle --> SetState: setState(value)
    SetState --> Idle: push present → past\nset present = value\nclear future (per behavior)

    Idle --> Undo: undo()
    Undo --> Idle: pop past → present\npush old present → future

    Idle --> Redo: redo()
    Redo --> Idle: pop future → present\npush old present → past

Visual walkthrough

Starting state:

{ past: [],              present: 0,    future: [] }

After setState(1)setState(2):

{ past: [0, 1],          present: 2,    future: [] }

After undo():

{ past: [0],             present: 1,    future: [2] }

After undo() again:

{ past: [],              present: 0,    future: [1, 2] }

After redo():

{ past: [0],             present: 1,    future: [2] }

API Reference

Hook signature

function useUndoable<T>(
  initialPresent: T,
  options?: Options
): UseUndoable<T>

Return value

The hook returns a tuple [state, setState, helpers]:

| Element | Type | Description | |---------|------|-------------| | state | T | The current (present) state value | | setState | (value \| updater, behavior?, ignoreAction?) => void | Updater; accepts a value or functional updater, like useState | | helpers.past | T[] | Array of past state values | | helpers.future | T[] | Array of future (redo-able) state values | | helpers.undo | () => void | Move one step back in history | | helpers.redo | () => void | Move one step forward in history | | helpers.canUndo | boolean | Whether undo is available | | helpers.canRedo | boolean | Whether redo is available | | helpers.reset | (newState?) => void | Wipe history and reset present (defaults to initialState) | | helpers.resetInitialState | (newInitial: T) => void | Replace the first item in past; useful for async data | | helpers.static_setState | (value, behavior?, ignoreAction?) => void | Stable setter that never changes identity (value only, no functional updater) |

setState parameters

setState(
  payload: T | ((prev: T) => T),  // new value or functional updater
  behavior?: MutationBehavior,     // override for this call only
  ignoreAction?: boolean           // if true, skip history tracking
)

Tip: To use the global behavior but set ignoreAction, pass null as the second argument: setState(value, null, true)


Options

interface Options {
  behavior?: MutationBehavior
  historyLimit?: number | "infinium" | "infinity"
  ignoreIdenticalMutations?: boolean
  cloneState?: boolean
}

| Option | Default | Description | |--------|---------|-------------| | behavior | "mergePastReversed" | How history is treated after a state change following an undo | | historyLimit | 100 | Max items in the past array. Use "infinium" or "infinity" for unlimited | | ignoreIdenticalMutations | true | Skip recording a state change if the new value equals the current value | | cloneState | false | Return a shallow clone instead of the same reference (helps trigger re-renders) |


Mutation Behaviors

The behavior determines what happens to the future array when you make a new state change after having called undo. This is the killer feature: you're not locked into one strategy.

Starting point for all examples:

{ past: [],  present: 0,  future: [1, 2] }

Now we call setState(5):

destroyFuture: discard the future

flowchart LR
    A["past: []<br/>present: 0<br/>future: [1, 2]"] -->|setState 5| B["past: [0]<br/>present: 5<br/>future: []"]
    style B fill:#ffcccc,stroke:#cc0000

The future is erased. Values 1 and 2 are gone; the user can't get back to them.

mergePastReversed (default): merge future into past (reversed)

flowchart LR
    A["past: []<br/>present: 0<br/>future: [1, 2]"] -->|setState 5| B["past: [0, 2, 1]<br/>present: 5<br/>future: []"]
    style B fill:#ccffcc,stroke:#00aa00

The future is reversed and appended to the past. Every state remains reachable via undo.

mergePast: merge future into past (as-is)

flowchart LR
    A["past: []<br/>present: 0<br/>future: [1, 2]"] -->|setState 5| B["past: [0, 1, 2]<br/>present: 5<br/>future: []"]
    style B fill:#ccffcc,stroke:#00aa00

Same as above, but the future is appended in its original order.

keepFuture: leave the future untouched

flowchart LR
    A["past: []<br/>present: 0<br/>future: [1, 2]"] -->|setState 5| B["past: [0]<br/>present: 5<br/>future: [1, 2]"]
    style B fill:#cce5ff,stroke:#0066cc

The future stays in place. Both undo and redo remain available.

Behavior comparison

| Behavior | Future after setState | All states reachable? | Use case | |----------|------------------------|----------------------|----------| | destroyFuture | [] | No | Git-style (branch is discarded) | | mergePastReversed | [] | Yes | Intuitive undo-then-edit (default) | | mergePast | [] | Yes | Same, but preserves future order | | keepFuture | unchanged | Yes | Non-destructive, keeps redo available |


Handling Async Data (resetInitialState)

When fetching data from an API, your initialState might start as [] or undefined. Without resetInitialState, users could undo all the way back to that empty state.

function TodoList() {
  const [todos, setTodos, { undo, redo, resetInitialState }] = useUndoable([])

  useEffect(() => {
    fetchTodos().then(apiTodos => {
      setTodos(apiTodos)
      resetInitialState(apiTodos) // ← prevents undo back to []
    })
  }, [])

  return /* ... */
}

Important: Call resetInitialState only once. Calling it multiple times on an existing state risks overwriting legitimate history entries.


static_setState

The default setState changes identity on every state update because it supports functional updaters (which close over present). If this causes issues with memoized children or effect dependencies, use static_setState:

const [count, setCount, { static_setState }] = useUndoable(0)

// static_setState accepts a value only (no functional updater)
// its identity never changes across renders
static_setState(42)

Renaming Destructured Values

Since the third element is an object, you can rename anything:

const [
  count,
  setCount,
  {
    past: history,
    future: upcoming,
    undo: stepBack,
    redo: stepForward,
    canUndo: hasHistory,
    reset: wipe,
  },
] = useUndoable(0)

Performance

Every state change stores the previous state in memory. For large state objects, consider:

  1. Use historyLimit to cap the number of stored past states
  2. Store diffs, not snapshots; instead of storing the full array, store mutation descriptions like { index: 5, field: "name", value: "infinium" }
  3. Use ignoreIdenticalMutations to avoid polluting history with no-op updates
// Limit history to 50 entries
const [state, setState] = useUndoable(initialState, {
  historyLimit: 50,
})

Integration with React Flow v10

React Flow v10 separates node and edge state. To integrate with useUndoable, make it the exclusive state manager for both; don't let React Flow's internal state fight with the hook.

Note: Dragging a node fires many onDrag events, each counted as a separate history entry. You may want to debounce or batch these.

See react-flow-example/src/App.jsx for a working integration.


Contributing

# Clone and install
git clone https://github.com/SujalXplores/use-undoable-next.git
cd use-undoable-next
pnpm install

# Build the library (runs automatically on install via prepare)
pnpm build

# Run the demo
cd demo && pnpm install && pnpm start

License

MIT © sujalxplores