DEV Community
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
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)