A tiny undo/redo utility package for JavaScript, React, Vue, and Svelte.
npm install @reddojs/react
npm install @reddojs/vue
npm install @reddojs/svelte
npm install @reddojs/core
You can also use Reddo.js via CDN:
// React
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/react@latest/+esm'
// Vue
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/vue@latest/+esm'
// Svelte
import { useHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/svelte@latest/+esm'
// Vanilla
import { createHistory } from 'https://cdn.jsdelivr.net/npm/@reddojs/core@latest/+esm'
A command object represents an action that can be executed and undone.
interface Command {
key?: string // Optional key to group related commands for coalescing
do: () => void // Function to execute the command
undo: () => void // Function to undo the command
}
| Property | Type | Description |
|---|---|---|
key |
string (optional) |
When set, consecutive commands with the same key are merged into a single undo operation. Useful for text input, color pickers, sliders, etc. |
do |
() => void |
The function that performs the action |
undo |
() => void |
The function that reverses the action |
Configuration options for the history manager.
interface HistoryOptions {
size?: number // Max commands in history (default: 30)
coalesce?: boolean // Merge consecutive commands with same key (default: true)
}
| Option | Type | Default | Description |
|---|---|---|---|
size |
number |
30 |
Maximum number of commands to keep in history |
coalesce |
boolean |
true |
When enabled, consecutive commands with the same key are merged on undo |
All adapters (useHistory) return:
| Property | Type | Description |
|---|---|---|
execute |
(cmd: Command) => void |
Execute a command and add it to history |
undo |
() => void |
Undo the last command |
redo |
() => void |
Redo the last undone command |
clear |
() => void |
Clear all undo/redo history |
canUndo |
boolean |
Whether there are commands to undo |
canRedo |
boolean |
Whether there are commands to redo |
import { useHistory } from '@reddojs/react'
import { useState } from 'react'
function App() {
const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
const [count, setCount] = useState(0)
const [color, setColor] = useState('#ffffff')
function increment() {
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({
key: 'color-change', // coalesces rapid color changes
do: () => setColor(newValue),
undo: () => setColor(oldValue),
})
}
return (
<div>
<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} onChange={changeColor} />
</div>
)
}
<script setup lang="ts">
import { ref } from 'vue'
import { useHistory } from '@reddojs/vue'
const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
const count = ref(0)
const color = ref('#ffffff')
function increment() {
execute({
do: () => count.value++,
undo: () => count.value--,
})
}
function changeColor(e: Event) {
const oldValue = color.value
const newValue = (e.target as HTMLInputElement).value
execute({
key: 'color-change',
do: () => (color.value = newValue),
undo: () => (color.value = oldValue),
})
}
</script>
<template>
<div>
<button @click="undo" :disabled="!canUndo">Undo</button>
<button @click="redo" :disabled="!canRedo">Redo</button>
<button @click="increment">Count: {{ count }}</button>
<input type="color" :value="color" @input="changeColor" />
</div>
</template>
<script lang="ts">
import { useHistory } from '@reddojs/svelte'
const { execute, undo, redo, canUndo, canRedo } = useHistory({ size: 100 })
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({
key: 'color-change',
do: () => (color = newValue),
undo: () => (color = oldValue),
})
}
</script>
<div>
<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>
import { createHistory } from '@reddojs/core'
const history = createHistory({ size: 100 })
let count = 0
function increment() {
history.execute({
do: () => {
count++
render()
},
undo: () => {
count--
render()
},
})
}
function render() {
document.getElementById('count').textContent = count
document.getElementById('undo').disabled = !history.canUndo
document.getElementById('redo').disabled = !history.canRedo
}
// Subscribe to history changes
history.subscribe(render)
document.getElementById('increment').onclick = increment
document.getElementById('undo').onclick = () => history.undo()
document.getElementById('redo').onclick = () => history.redo()