Handling Undo/Redo in a form

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 -
1//Create a react app using the following command
2npm create vite@latest undo-redo-in-form --template react-ts
3cd undo-redo-in-form
4npm install
5
6//Install the Material UI library
7npm install @mui/material @emotion/react @emotion/styled
8
9//Command for run the React project
10npm run dev1. Folder Structure:

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.
1//useHistory.ts
2
3import { useState, useEffect } from "react";
4
5const useHistory = <T extends object>(
6 formData: T,
7 setFormData: React.Dispatch<React.SetStateAction<T>>
8) => {
9 const [history, setHistory] = useState<T[]>([formData]);
10 const [historyIndex, setHistoryIndex] = useState<number>(0);
11
12 useEffect(() => {
13 const savedHistory = localStorage.getItem("formHistory");
14 const savedIndex = localStorage.getItem("historyIndex");
15
16 if (savedHistory && savedIndex !== null) {
17 const parsedHistory: T[] = JSON.parse(savedHistory);
18 const parsedIndex = Number(savedIndex);
19 setHistory(parsedHistory);
20 setHistoryIndex(parsedIndex);
21 setFormData(parsedHistory[parsedIndex]);
22 }
23 }, []);
24
25 useEffect(() => {
26 localStorage.setItem("formHistory", JSON.stringify(history));
27 localStorage.setItem("historyIndex", historyIndex.toString());
28 }, [history, historyIndex]);
29
30 const setState = (newState: T): void => {
31 const updatedHistory = [...history.slice(0, historyIndex + 1), newState];
32 setHistory(updatedHistory);
33 setHistoryIndex(updatedHistory.length - 1);
34 setFormData(newState);
35 };
36
37 const undo = () => {
38 if (historyIndex > 0) {
39 const newIndex = historyIndex - 1;
40 setHistoryIndex(newIndex);
41 setFormData(history[newIndex]);
42 }
43 };
44
45 const redo = () => {
46 if (historyIndex < history.length - 1) {
47 const newIndex = historyIndex + 1;
48 setHistoryIndex(newIndex);
49 setFormData(history[newIndex]);
50 }
51 };
52
53 const undoAll = () => {
54 setHistoryIndex(0);
55 setFormData(history[0]);
56 };
57 const redoAll = () => {
58 const lastIndex = history.length - 1;
59 setHistoryIndex(lastIndex);
60 setFormData(history[lastIndex]);
61 };
62
63 const updateNestedState = (path: string, value: any): void => {
64 const newState: T = JSON.parse(JSON.stringify(formData));
65 const keys = path.split(".");
66 let temp: any = newState;
67
68 for (let i = 0; i < keys.length - 1; i++) {
69 temp = temp[keys[i]];
70 }
71
72 temp[keys[keys.length - 1]] = value;
73 setState(newState);
74 };
75
76 return {
77 setState,
78 undo,
79 redo,
80 undoAll,
81 redoAll,
82 updateNestedState,
83 };
84};
85
86export default useHistory;3. Form Component Implementation
1//Form.tsx
2import React, { useState } from "react";
3import {
4 Box,
5 TextField,
6 Button,
7 Typography,
8 RadioGroup,
9 FormControlLabel,
10 Radio,
11 Checkbox,
12 Select,
13 MenuItem,
14 InputLabel,
15 FormControl,
16 FormGroup,
17 TextareaAutosize,
18 Stack,
19} from "@mui/material";
20import useHistory from "../hooks/useHistory";
21
22interface FormData {
23 name: string;
24 email: string;
25 address: {
26 city: string;
27 };
28 gender: string;
29 skills: string[];
30 country: string;
31 bio: string;
32}
33const Form: React.FC = () => {
34 const initialFormData: FormData = {
35 name: "",
36 email: "",
37 address: { city: "" },
38 gender: "",
39 skills: [],
40 country: "",
41 bio: "",
42 };
43
44 const [formData, setFormData] = useState<FormData>(initialFormData);
45
46 const {
47 setState,
48 undo,
49 redo,
50 undoAll,
51 redoAll,
52 updateNestedState,
53 } = useHistory<FormData>(formData, setFormData);
54
55 //For skills section
56 const handleCheckboxChange = (skill: string) => {
57 const updatedSkills = formData.skills.includes(skill)
58 ? formData.skills.filter((s) => s !== skill)
59 : [...formData.skills, skill];
60 setState({ ...formData, skills: updatedSkills });
61 };
62
63 // Reset undo/redo history with cleared form
64 const handleSubmit = () => {
65 console.log("Form Submitted:", formData);
66 setFormData(initialFormData);
67 setState(initialFormData);
68 };
69
70 return (
71 <Box sx={{ maxWidth: 600, margin: "auto", p: 4 }}>
72 <Typography variant="h4" gutterBottom>
73 User Registration Form
74 </Typography>
75
76 <Stack spacing={3}>
77 <TextField
78 fullWidth
79 label="Name"
80 value={formData.name}
81 onChange={(e) => setState({ ...formData, name: e.target.value })}
82 />
83
84 <TextField
85 fullWidth
86 label="Email"
87 type="email"
88 value={formData.email}
89 onChange={(e) => setState({ ...formData, email: e.target.value })}
90 />
91
92 <TextField
93 fullWidth
94 label="City"
95 value={formData.address.city}
96 onChange={(e) => updateNestedState("address.city", e.target.value)}
97 />
98
99 <Box>
100 <Typography variant="subtitle1">Gender</Typography>
101 <RadioGroup
102 row
103 value={formData.gender}
104 onChange={(e) => setState({ ...formData, gender: e.target.value })}
105 >
106 <FormControlLabel value="male" control={<Radio />} label="Male" />
107 <FormControlLabel value="female" control={<Radio />} label="Female" />
108 </RadioGroup>
109 </Box>
110
111 <Box>
112 <Typography variant="subtitle1">Skills</Typography>
113 <FormGroup row>
114 {["React", "Node", "CSS", "JavaScript"].map((skill) => (
115 <FormControlLabel
116 key={skill}
117 control={
118 <Checkbox
119 checked={formData.skills.includes(skill)}
120 onChange={() => handleCheckboxChange(skill)}
121 />
122 }
123 label={skill}
124 />
125 ))}
126 </FormGroup>
127 </Box>
128
129 <FormControl fullWidth>
130 <InputLabel>Country</InputLabel>
131 <Select
132 value={formData.country}
133 label="Country"
134 onChange={(e) => setState({ ...formData, country: e.target.value })}
135 >
136 <MenuItem value="India">India</MenuItem>
137 <MenuItem value="USA">USA</MenuItem>
138 <MenuItem value="UK">UK</MenuItem>
139 </Select>
140 </FormControl>
141
142 <FormControl fullWidth>
143 <TextareaAutosize
144 minRows={4}
145 placeholder="Bio"
146 value={formData.bio}
147 onChange={(e) => setState({ ...formData, bio: e.target.value })}
148 style={{ width: "100%", padding: 10, fontSize: 16 }}
149 />
150 </FormControl>
151
152 <Stack direction="row" spacing={2}>
153 <Button variant="outlined" onClick={undo}>
154 Undo
155 </Button>
156 <Button variant="outlined" onClick={redo}>
157 Redo
158 </Button>
159 <Button variant="contained" onClick={undoAll}>
160 Undo All
161 </Button>
162 <Button variant="contained" onClick={redoAll}>
163 Redo All
164 </Button>
165 </Stack>
166 <Button variant="contained" color="primary" onClick={handleSubmit}>
167 Submit
168 </Button>
169 </Stack>
170 </Box>
171 );
172};
173
174export 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.