Framework-Agnostic Undo/Redo History Management Library – Reddo.js

Category: Javascript , Recommended | January 4, 2026
Authoreihabkhan
Last UpdateJanuary 4, 2026
LicenseMIT
Views20 views
Framework-Agnostic Undo/Redo History Management Library – Reddo.js

Reddo.js is a lightweight, framework-agnostic utility library that manages undo/redo history management across various JavaScript environments.

It abstracts the complex logic of maintaining history stacks, handling pointers, and grouping related actions into a simple, unified API.

The library weighs less than 1KB gzipped and works with React, Vue, Svelte, or Vanilla JavaScript.

Features:

  • Command coalescing: Consecutive commands with matching keys merge into single undo operations.
  • Configurable history size: You can set a maximum number of commands to retain.

Use Cases:

  • Rich Text Editors: You expect typing a sentence to be one “undo” action rather than removing one character at a time. Reddo.js handles this via coalescing.
  • Graphic Design Tools: It manages the state of canvas elements. You can move objects and revert the position easily.
  • Complex Configuration Forms: It allows you to experiment with settings in a multi-step wizard. They can revert changes safely.
  • Data Grids: It tracks cell edits. This permits you to roll back bulk changes to data tables.

Installation:

1. Install the Reddo.js package based on your project setup.

React:

npm install @reddojs/react

Vue:

npm install @reddojs/vue

Svelte:

npm install @reddojs/svelte

Vanilla JavaScript:

npm install @reddojs/core

2. You can also load Reddo.js from a CDN for quick prototyping or testing.

// React CDN import
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/react@latest/+esm'
// Vue CDN import
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/vue@latest/+esm'
// Svelte CDN import
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/svelte@latest/+esm'
// Vanilla JS CDN import
import { createHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/core@latest/+esm'

Vanilla JavaScript Implementation

The core library works in any JavaScript environment with explicit render calls.

import { createHistory } from '@reddojs/core'
// Create history instance with options
const history = createHistory({ size: 100 })
// Application state lives outside the library
let count = 0
function increment() {
  history.execute({
    do: () => {
      count++
      // Manually trigger render after state change
      render()
    },
    undo: () => {
      count--
      render()
    },
  })
}
function render() {
  // Update DOM elements with current state
  document.getElementById('count').textContent = count
  
  // Update button disabled states based on history
  document.getElementById('undo').disabled = !history.canUndo
  document.getElementById('redo').disabled = !history.canRedo
}
// Subscribe to history changes for automatic updates
history.subscribe(render)
// Wire up event handlers
document.getElementById('increment').onclick = increment
document.getElementById('undo').onclick = () => history.undo()
document.getElementById('redo').onclick = () => history.redo()

Basic React Implementation

Here’s a complete React example showing counter and color picker with undo/redo support.

import { useHistory } from '@reddojs/react'
import { useState } from 'react'
function App() {
  // Initialize history with 100 command limit
  const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
  
  // Local state for counter and color
  const [count, setCount] = useState(0)
  const [color, setColor] = useState('#ffffff')
  function increment() {
    // Execute command with do and undo functions
    execute({
      do: () => setCount(prev => prev + 1),
      undo: () => setCount(prev => prev - 1),
    })
  }
  function changeColor(e: React.ChangeEvent<HTMLInputElement>) {
    const oldValue = color
    const newValue = e.target.value
    execute({
      // Commands with same key get coalesced
      key: 'color-change',
      do: () => setColor(newValue),
      undo: () => setColor(oldValue),
    })
  }
  return (
    <div>
      {/* Disable buttons when no history available */}
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
      
      <button onClick={increment}>Count: {count}</button>
      
      {/* Color changes coalesce automatically */}
      <input type="color" value={color} onChange={changeColor} />
    </div>
  )
}

Vue 3 Implementation

The Vue adapter uses the Composition API and provides reactive canUndo and canRedo values.

<script setup lang="ts">
import { ref } from 'vue'
import { useHistory } from '@reddojs/vue'
// Initialize with custom history size
const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
// Reactive refs for state
const count = ref(0)
const color = ref('#ffffff')
function increment() {
  execute({
    // Increment counter in do function
    do: () => count.value++,
    // Decrement counter in undo function
    undo: () => count.value--,
  })
}
function changeColor(e: Event) {
  const oldValue = color.value
  const newValue = (e.target as HTMLInputElement).value
  execute({
    // Same key groups rapid color changes
    key: 'color-change',
    do: () => (color.value = newValue),
    undo: () => (color.value = oldValue),
  })
}
</script>
<template>
  <div>
    {/* Boolean props work directly with Vue */}
    <button @click="undo" :disabled="!canUndo">Undo</button>
    <button @click="redo" :disabled="!canRedo">Redo</button>
    
    <button @click="increment">Count: {{ count }}</button>
    
    {/* Input event triggers on every change */}
    <input type="color" :value="color" @input="changeColor" />
  </div>
</template>

Svelte Implementation

Svelte’s adapter uses runes syntax for reactive state in Svelte 5+.

<script lang="ts">
  import { useHistory } from '@reddojs/svelte'
  // Get history management functions
  const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
  
  // Svelte 5 runes for reactive state
  let count = $state(0)
  let color = $state('#ffffff')
  function increment() {
    execute({
      do: () => count++,
      undo: () => count--,
    })
  }
  function changeColor(e: Event) {
    const oldValue = color
    const newValue = (e.target as HTMLInputElement).value
    execute({
      // Commands with matching keys coalesce
      key: 'color-change',
      do: () => (color = newValue),
      undo: () => (color = oldValue),
    })
  }
</script>
<div>
  {/* Svelte reactive boolean checks */}
  <button onclick={undo} disabled={!canUndo}>Undo</button>
  <button onclick={redo} disabled={!canRedo}>Redo</button>
  
  <button onclick={increment}>Count: {count}</button>
  
  <input type="color" value={color} oninput={changeColor} />
</div>

Configuration Options:

  • size (number): Maximum number of commands to keep in history. Default is 30. When the limit is reached, the oldest command is removed to make room for new ones.
  • coalesce (boolean): Merge consecutive commands with matching keys into single undo operations. Default is true. This prevents users from needing to undo every individual change in rapid sequences like typing or slider adjustments.

Command Interface:

Commands define reversible actions through do and undo functions.

  • key (string, optional): Identifier for grouping related commands. When coalescing is on, consecutive commands with the same key merge on undo. This is useful for text inputs, color pickers, range sliders, and other continuous input controls.
  • do (() => void): Function that executes the forward action. This runs immediately when you call execute() and again if the user redoes the action.
  • undo (() => void): Function that reverses the action. This runs when the user clicks undo or calls the undo() method. With coalescing on, multiple undo functions may execute together for commands sharing a key.

History Methods:

// Execute a command and add it to history
// This runs cmd.do() immediately and pushes to the undo stack
execute(cmd)
// Undo the most recent command or coalesced group
// Pops from undo stack and pushes to redo stack
undo()
// Redo the most recently undone command
// Pops from redo stack and pushes to undo stack
redo()
// Clear all undo and redo history
// Resets both stacks to empty
clear()

History State Properties:

// Boolean indicating if undo is available
// True when undo stack has commands
canUndo
// Boolean indicating if redo is available
// True when redo stack has commands
canRedo

History Subscription (Vanilla JS only):

// Subscribe to history state changes
// Callback fires after execute, undo, redo, or clear
const unsubscribe = history.subscribe(() =&gt; {
  // Update UI based on new history state
  updateButtonStates()
})
// Later: remove the subscription
unsubscribe()

Alternatives:

  • Immer: The most popular immutable state library with produce() API.
  • Travels.js: 10x Faster Undo/Redo Using JSON Patches

FAQs:

Q: How do I prevent memory leaks in long-running applications with many undo operations?
A: Set an appropriate size limit when initializing the history manager. The default is 30 commands, but you can adjust based on your app’s needs. Once the limit is reached, the oldest command is automatically removed.

Q: Can I integrate Reddo.js with existing Redux or MobX state management?
A: Yes. Execute commands that dispatch Redux actions or modify MobX observables. In the do function, dispatch your action or update the observable. In the undo function, dispatch the inverse action or restore the previous value. You’ll need to capture the old state before the change to enable proper reversal.

Q: Why do some of my undo operations skip multiple steps at once?
A: This happens when command coalescing is active and multiple consecutive commands share the same key. The library groups them into a single undo operation. This is intentional for inputs like text fields and sliders. If you want every change to undo separately, either omit the key property or set coalesce: false in the history options.

Q: How do I handle async operations in commands?
A: The do and undo functions are synchronous. For async operations, execute them outside the command and create the command only after completion. Store any necessary data from the async result in your component state, then create a command that synchronously updates that state. This keeps the history stack predictable and prevents race conditions.

Q: Does Reddo.js work with class components in React?
A: The @reddojs/react package is hook-based and requires functional components. For class components, import @reddojs/core directly and manage the history instance in component state. Call history methods from event handlers and use history.subscribe() in componentDidMount to trigger setState when history changes. Remember to unsubscribe in componentWillUnmount.

You Might Be Interested In:


Leave a Reply