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

Image of Stellar post

How a Hackathon Win Led to My Startup Getting Funded

In this episode, you'll see:

  • The hackathon wins that sparked the journey.
  • The moment José and Joseph decided to go all-in.
  • Building a working prototype on Stellar.
  • Using the PassKeys feature of Soroban.
  • Getting funded via the Stellar Community Fund.

Watch the video 🎥

Top comments (0)

Image of Stellar post

🚀 Stellar Dev Diaries Series: Episode 1 is LIVE!

Ever wondered what it takes to build a web3 startup from scratch? In the Stellar Dev Diaries series, we follow the journey of a team of developers building on the Stellar Network as they go from hackathon win to getting funded and launching on mainnet.

Read more

👋 Kindness is contagious

Please stick around with the Forem app — the best way to keep up with DEV and other tech communities.

Let's go