DEV Community

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

Posted on • Edited on

2 1

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.

For a much more extensive guide on getting the most out of React portals, check out my full 24-page PDF file on Gumroad. It's available for just $10:

Using React Portals Like a Pro.

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

Heroku

Built for developers, by developers.

Whether you're building a simple prototype or a business-critical product, Heroku's fully-managed platform gives you the simplest path to delivering apps quickly — using the tools and languages you already love!

Learn More

Top comments (0)

ACI image

ACI.dev: The Only MCP Server Your AI Agents Need

ACI.dev’s open-source tool-use platform and Unified MCP Server turns 600+ functions into two simple MCP tools on one server—search and execute. Comes with multi-tenant auth and natural-language permission scopes. 100% open-source under Apache 2.0.

Star our GitHub!

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay