Introduction
An undo/redo functionality in the form building area can be more useful for the end user, particularly when applications are complex and users do many changes and might want to undo some actions. In this comprehensive guide, we are going to see how to implement solid undo/redo functionality for React forms.
Prerequisites
React Fundamentals
State Management in React
Form Development with Material-UI
JavaScript / TypeScript concepts
Why Undo/Redo Matters in Forms
Form interactions can be complex, and users often make mistakes or change their minds. Undo/redo functionality provides :
Error Recovery : Users can quickly revert accidental changes
Confidence : Knowing they can undo changes encourages users to experiment
Efficiency : Faster than manually reverting multiple field changes
Professional UX : Makes your application feel more polished and user-friendly
Core Concepts
- State History Management
The foundation of undo/redo is maintaining a history of form states. Each significant change creates a new entry in the history stack, allowing navigation between different versions of the form data.
- Command Pattern Implementation
Each user action becomes a reversible command that knows how to execute and undo itself. This creates clean separation between actions and their effects.
- Memory Optimization
Since form data can be large, we need strategies to prevent memory bloat while maintaining sufficient history depth.
Code Implementation
Let’s create a React app using the following steps :
//Create a react app using the following command
npm create vite@latest undo-redo-in-form --template react-ts
cd undo-redo-in-form
npm install
//Install the Material UI library
npm install @mui/material @emotion/react @emotion/styled
//Command for run the React project
npm run dev
Step-1 : Folder Structure :

Step-2 : Custom Hook Approach :
Let us implement a custom hook in React that can assist our state management for form histories and provide undo/redo actions. We will divide this as follows :
- State Management : We will keep two pieces of state: one for the form-data history and another for the current position (or index) in the history stack.
- Persistence : The state should be persisted via localStorage so that even after the user refreshes, the history can still be accessed.
- Undo/Redo : Functions for traversing backwards and forwards through history.
//useHistory.ts
import { useState, useEffect } from "react";
const useHistory = (
formData: T,
setFormData: React.Dispatch>
) => {
const [history, setHistory] = useState([formData]);
const [historyIndex, setHistoryIndex] = useState(0);
useEffect(() => {
const savedHistory = localStorage.getItem("formHistory");
const savedIndex = localStorage.getItem("historyIndex");
if (savedHistory && savedIndex !== null) {
const parsedHistory: T[] = JSON.parse(savedHistory);
const parsedIndex = Number(savedIndex);
setHistory(parsedHistory);
setHistoryIndex(parsedIndex);
setFormData(parsedHistory[parsedIndex]);
}
}, []);
useEffect(() => {
localStorage.setItem("formHistory", JSON.stringify(history));
localStorage.setItem("historyIndex", historyIndex.toString());
}, [history, historyIndex]);
const setState = (newState: T): void => {
const updatedHistory = [...history.slice(0, historyIndex + 1), newState];
setHistory(updatedHistory);
setHistoryIndex(updatedHistory.length - 1);
setFormData(newState);
};
const undo = () => {
if (historyIndex > 0) {
const newIndex = historyIndex - 1;
setHistoryIndex(newIndex);
setFormData(history[newIndex]);
}
};
const redo = () => {
if (historyIndex < history.length - 1) {
const newIndex = historyIndex + 1;
setHistoryIndex(newIndex);
setFormData(history[newIndex]);
}
};
const undoAll = () => {
setHistoryIndex(0);
setFormData(history[0]);
};
const redoAll = () => {
const lastIndex = history.length - 1;
setHistoryIndex(lastIndex);
setFormData(history[lastIndex]);
};
const updateNestedState = (path: string, value: any): void => {
const newState: T = JSON.parse(JSON.stringify(formData));
const keys = path.split(".");
let temp: any = newState;
for (let i = 0; i < keys.length - 1; i++) {
temp = temp[keys[i]];
}
temp[keys[keys.length - 1]] = value;
setState(newState);
};
return {
setState,
undo,
redo,
undoAll,
redoAll,
updateNestedState,
};
};
export default useHistory;
Step-3 : Form Component Implementation
//Form.tsx
import React, { useState } from "react";
import {
Box,
TextField,
Button,
Typography,
RadioGroup,
FormControlLabel,
Radio,
Checkbox,
Select,
MenuItem,
InputLabel,
FormControl,
FormGroup,
TextareaAutosize,
Stack,
} from "@mui/material";
import useHistory from "../hooks/useHistory";
interface FormData {
name: string;
email: string;
address: {
city: string;
};
gender: string;
skills: string[];
country: string;
bio: string;
}
const Form: React.FC = () => {
const initialFormData: FormData = {
name: "",
email: "",
address: { city: "" },
gender: "",
skills: [],
country: "",
bio: "",
};
const [formData, setFormData] = useState(initialFormData);
const {
setState,
undo,
redo,
undoAll,
redoAll,
updateNestedState,
} = useHistory(formData, setFormData);
//For skills section
const handleCheckboxChange = (skill: string) => {
const updatedSkills = formData.skills.includes(skill)
? formData.skills.filter((s) => s !== skill)
: [...formData.skills, skill];
setState({ ...formData, skills: updatedSkills });
};
// Reset undo/redo history with cleared form
const handleSubmit = () => {
console.log("Form Submitted:", formData);
setFormData(initialFormData);
setState(initialFormData);
};
return (
User Registration Form
setState({ ...formData, name: e.target.value })}
/>
setState({ ...formData, email: e.target.value })}
/>
updateNestedState("address.city", e.target.value)}
/>
Gender
setState({ ...formData, gender: e.target.value })}
>
} label="Male" />
} label="Female" />
Skills
{["React", "Node", "CSS", "JavaScript"].map((skill) => (
handleCheckboxChange(skill)}
/>
}
label={skill}
/>
))}
Country
setState({ ...formData, bio: e.target.value })}
style={{ width: "100%", padding: 10, fontSize: 16 }}
/>
);
};
export default Form;
With the above code, you will get the following output on your screen.

Conclusion
Implementing undo/redo functionality in React forms enhances user experience significantly. The key is to balance functionality with performance, provide clear visual feedback, and ensure the implementation is intuitive for users.