ReactJS

Handling Undo/Redo in a form

Ridham Kansara
Ridham KansaraJul 1, 2025

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 dev

1. Folder Structure:

Blog Image

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.

Blog Image

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.

© 2026 IGNEK. All rights reserved.

Ignek on LinkedInIgnek on InstagramIgnek on FacebookIgnek on YouTubeIgnek on X