Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

DEV Community

Cover image for How to Build a Persistent Undo/Redo Stack in React Without Redux
HexShift
HexShift

Posted on

How to Build a Persistent Undo/Redo Stack in React Without Redux

Undo/redo functionality isn't just for text editors — it's critical for rich apps like form builders, design tools, and config editors. Here's how to build a fully working persistent undo/redo stack in React using only hooks and context — no Redux, no Zustand.

Why Build an Undo/Redo Stack?

Common use cases:

  • Recover user mistakes easily
  • Improve UX for complex editing flows
  • Enable "draft" save systems with full history

Step 1: Create the Undo Context

This context will track a history of states and provide undo/redo functions:

// undoContext.js
import { createContext, useContext, useState } from "react";

const UndoContext = createContext(null);

export function UndoProvider({ children }) {
  const [history, setHistory] = useState([]);
  const [currentIndex, setCurrentIndex] = useState(-1);

  const record = (newState) => {
    const newHistory = history.slice(0, currentIndex + 1);
    newHistory.push(newState);
    setHistory(newHistory);
    setCurrentIndex(newHistory.length - 1);
  };

  const undo = () => {
    if (currentIndex > 0) setCurrentIndex(currentIndex - 1);
  };

  const redo = () => {
    if (currentIndex < history.length - 1) setCurrentIndex(currentIndex + 1);
  };

  const current = history[currentIndex] || null;

  return (
    <UndoContext.Provider value={{ record, undo, redo, current }}>
      {children}
    </UndoContext.Provider>
  );
}

export function useUndo() {
  return useContext(UndoContext);
}

Step 2: Build an Editable Component

Let's make a simple editable text input that records its history:

// EditableInput.js
import { useUndo } from "./undoContext";
import { useState, useEffect } from "react";

function EditableInput() {
  const { record, current } = useUndo();
  const [value, setValue] = useState("");

  useEffect(() => {
    if (current !== null) {
      setValue(current);
    }
  }, [current]);

  const handleChange = (e) => {
    setValue(e.target.value);
    record(e.target.value);
  };

  return <input value={value} onChange={handleChange} placeholder="Type something..." />;
}

export default EditableInput;

Step 3: Add Undo/Redo Buttons

Control the undo/redo from anywhere in your app:

// UndoRedoControls.js
import { useUndo } from "./undoContext";

function UndoRedoControls() {
  const { undo, redo } = useUndo();

  return (
    <div>
      <button onClick={undo}>Undo</button>
      <button onClick={redo}>Redo</button>
    </div>
  );
}

export default UndoRedoControls;

Step 4: Wrap the App with the UndoProvider

// App.js
import { UndoProvider } from "./undoContext";
import EditableInput from "./EditableInput";
import UndoRedoControls from "./UndoRedoControls";

function App() {
  return (
    <UndoProvider>
      <EditableInput />
      <UndoRedoControls />
    </UndoProvider>
  );
}

export default App;

Pros and Cons

✅ Pros

  • Lightweight — no third-party dependencies
  • Fully persistent history stack
  • Easy to expand to more complex states

⚠️ Cons

  • Memory usage grows if history isn't trimmed
  • Best for small/medium states — large states might need diffing
  • No batching of similar actions

🚀 Alternatives

  • Zustand with middleware for undo/redo
  • use-undo npm package (small and focused)

Summary

Undo/redo isn't hard — it's just careful state tracking. With this context-based setup, you can add reliable undo features to your React apps without reaching for heavy global state managers. Great for creative tools, live editors, and productivity apps.

If you found this helpful, you can support me here: buymeacoffee.com/hexshift

Top comments (0)