← Back to all hooks

useUndoRedo

Time-travel state management with undo/redo functionality. Perfect for editors, drawing apps, and form builders.

Live Demo

Draw shapes and use undo/redo to travel through history. Click any history state to jump to it.

šŸŽØ Drawing Canvas

Click buttons below to add shapes

ā®ļø Time Travel Controls

Shapes: 0
History: 1 / 1
Can Undo: āŒ | Can Redo: āŒ

šŸ“œ History Timeline

šŸ’” How It Works:

  • Every state change is saved to history
  • Navigate backward and forward through states
  • Jump to any point in history
  • New changes clear future history
  • Configurable max history size

Code Examples

Basic Usage

import { useUndoRedo } from "@uiblock/hooks";

function TextEditor() {
  const {
    state,
    setState,
    undo,
    redo,
    canUndo,
    canRedo
  } = useUndoRedo({
    initialState: { text: '' },
    maxHistory: 50
  });

  return (
    <div>
      <textarea
        value={state.text}
        onChange={(e) => setState({ text: e.target.value })}
      />
      <button onClick={undo} disabled={!canUndo}>Undo</button>
      <button onClick={redo} disabled={!canRedo}>Redo</button>
    </div>
  );
}

Form Builder

import { useUndoRedo } from "@uiblock/hooks";

interface FormField {
  id: string;
  type: 'text' | 'email' | 'number';
  label: string;
}

function FormBuilder() {
  const { state, setState, undo, redo, canUndo, canRedo } = useUndoRedo<{
    fields: FormField[]
  }>({
    initialState: { fields: [] }
  });

  const addField = (type: FormField['type']) => {
    setState(prev => ({
      fields: [...prev.fields, {
        id: Date.now().toString(),
        type,
        label: `New ${type} field`
      }]
    }));
  };

  const removeField = (id: string) => {
    setState(prev => ({
      fields: prev.fields.filter(f => f.id !== id)
    }));
  };

  return (
    <div>
      <button onClick={() => addField('text')}>Add Text Field</button>
      <button onClick={undo} disabled={!canUndo}>↶ Undo</button>
      <button onClick={redo} disabled={!canRedo}>↷ Redo</button>
      
      {state.fields.map(field => (
        <div key={field.id}>
          <input type={field.type} placeholder={field.label} />
          <button onClick={() => removeField(field.id)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

History Timeline

import { useUndoRedo } from "@uiblock/hooks";

function HistoryViewer() {
  const {
    state,
    history,
    currentIndex,
    goToState
  } = useUndoRedo({
    initialState: { count: 0 }
  });

  return (
    <div>
      <p>Current: {state.count}</p>
      <div>
        {history.map((historyState, index) => (
          <button
            key={index}
            onClick={() => goToState(index)}
            style={{
              fontWeight: index === currentIndex ? 'bold' : 'normal'
            }}
          >
            State {index}: {historyState.count}
          </button>
        ))}
      </div>
    </div>
  );
}

Keyboard Shortcuts

import { useUndoRedo } from "@uiblock/hooks";
import { useEffect } from "react";

function EditorWithShortcuts() {
  const { state, setState, undo, redo, canUndo, canRedo } = useUndoRedo({
    initialState: { content: '' }
  });

  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'z') {
        e.preventDefault();
        if (e.shiftKey) {
          redo();
        } else {
          undo();
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [undo, redo]);

  return (
    <textarea
      value={state.content}
      onChange={(e) => setState({ content: e.target.value })}
      placeholder="Try Cmd/Ctrl+Z to undo, Cmd/Ctrl+Shift+Z to redo"
    />
  );
}

How It Works

Here's the implementation:

import { useState, useCallback, useRef } from 'react'

export function useUndoRedo<T>(options: {
  maxHistory?: number
  initialState: T
}) {
  const { maxHistory = 50, initialState } = options

  const [history, setHistory] = useState<T[]>([initialState])
  const [currentIndex, setCurrentIndex] = useState(0)
  const isUndoRedoRef = useRef(false)

  const state = history[currentIndex]

  const setState = useCallback((newState: T | ((prev: T) => T)) => {
    if (isUndoRedoRef.current) {
      isUndoRedoRef.current = false
      return
    }

    setHistory(currentHistory => {
      const resolvedState = typeof newState === 'function'
        ? (newState as (prev: T) => T)(currentHistory[currentIndex])
        : newState

      // Remove any future history when making a new change
      const newHistory = currentHistory.slice(0, currentIndex + 1)
      newHistory.push(resolvedState)

      // Limit history size
      if (newHistory.length > maxHistory) {
        newHistory.shift()
        setCurrentIndex(currentIndex)
        return newHistory
      }

      setCurrentIndex(newHistory.length - 1)
      return newHistory
    })
  }, [currentIndex, maxHistory])

  const undo = useCallback(() => {
    if (currentIndex > 0) {
      isUndoRedoRef.current = true
      setCurrentIndex(currentIndex - 1)
    }
  }, [currentIndex])

  const redo = useCallback(() => {
    if (currentIndex < history.length - 1) {
      isUndoRedoRef.current = true
      setCurrentIndex(currentIndex + 1)
    }
  }, [currentIndex, history.length])

  const reset = useCallback(() => {
    setHistory([initialState])
    setCurrentIndex(0)
  }, [initialState])

  const goToState = useCallback((index: number) => {
    if (index >= 0 && index < history.length) {
      isUndoRedoRef.current = true
      setCurrentIndex(index)
    }
  }, [history.length])

  return {
    state,
    setState,
    undo,
    redo,
    canUndo: currentIndex > 0,
    canRedo: currentIndex < history.length - 1,
    reset,
    history,
    currentIndex,
    goToState
  }
}

API Reference

useUndoRedo(options)

initialState: T

Initial state value

maxHistory?: number

Maximum history size (default: 50)

Returns:

  • state: Current state
  • setState: Update state function
  • undo: Go back one step
  • redo: Go forward one step
  • canUndo: Boolean if undo is available
  • canRedo: Boolean if redo is available
  • reset: Reset to initial state
  • history: Array of all states
  • currentIndex: Current position in history
  • goToState: Jump to specific history index

šŸ’” Use Cases

  • Text editors and rich text editors
  • Drawing and design tools
  • Form builders and visual editors
  • Game state management
  • Configuration editors
  • Any app needing undo/redo functionality