Reddojs
A tiny Undo/Redo Library for JavaScript, React, Vue, and Svelte
Install / Use
/learn @eihabkhan/ReddojsREADME
Reddo.js
<img width="148" height="132" alt="Reddojs" src="https://github.com/user-attachments/assets/5231621c-d738-498e-878c-e0c2ae8c4746" align="right" />A tiny undo/redo utility package for JavaScript, React, Vue, and Svelte.
- Tiny. Less than 1kb (gzipped). No dependencies.
- Zero dependencies Core lib has no runtime deps
- Dead simple Just import hook, execute, undo, redo
- Framework agnostic Core works anywhere, with official React, Vue & Svelte adapters
- TypeScript first Fully typed.
- Command coalescing Automatically groups related commands (e.g., typing in a text field, changing a color in a color picker)
Table of Contents
Installation
React
npm install @reddojs/react
Vue
npm install @reddojs/vue
Svelte
npm install @reddojs/svelte
Vanilla
npm install @reddojs/core
CDN
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'
API Reference
Command
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 |
HistoryOptions
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 |
Return Values
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 |
Usage
React
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>
)
}
Vue
<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>
Svelte
<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>
Vanilla
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()
