Canvas Undo and Redo with React and Konva
Canvas pixels do not contain the application history. Store history in the data
model that produces the canvas scene.
For small editors, store an immutable snapshot after each complete user action.
Keep a current history index. Remove future snapshots after a new edit that
follows an undo.
Do not store Konva node instances in history. Store plain application data such
as positions, colors, text, and stable identifiers.
Snapshot history in React
Drag the rectangle. Each completed drag adds one snapshot. The Undo and Redo
buttons change the current history index.
import { useRef, useState } from 'react';
import { Stage, Layer, Rect, Text } from 'react-konva';
const initialRectangle = {
id: 'rectangle-1',
x: 80,
y: 100,
width: 170,
height: 110,
fill: '#4dabf7',
};
const App = () => {
const history = useRef([initialRectangle]);
const historyIndex = useRef(0);
const [rectangle, setRectangle] = useState(initialRectangle);
const commit = (nextRectangle) => {
const previousSnapshots = history.current.slice(0, historyIndex.current + 1);
history.current = [...previousSnapshots, nextRectangle];
historyIndex.current = history.current.length - 1;
setRectangle(nextRectangle);
};
const undo = () => {
if (historyIndex.current === 0) {
return;
}
historyIndex.current -= 1;
setRectangle(history.current[historyIndex.current]);
};
const redo = () => {
if (historyIndex.current === history.current.length - 1) {
return;
}
historyIndex.current += 1;
setRectangle(history.current[historyIndex.current]);
};
return (
<>
<button onClick={undo} disabled={historyIndex.current === 0}>
Undo
</button>
<button
onClick={redo}
disabled={historyIndex.current === history.current.length - 1}
>
Redo
</button>
<Stage width={window.innerWidth} height={380}>
<Layer>
<Text x={20} y={20} text="Drag the rectangle" fontSize={18} />
<Rect
{...rectangle}
stroke="#1e3a5f"
strokeWidth={3}
cornerRadius={10}
draggable
onDragEnd={(event) => {
commit({
...rectangle,
x: event.target.x(),
y: event.target.y(),
});
}}
/>
</Layer>
</Stage>
</>
);
};
export default App;
Commit one history entry for one user action. Do not add an entry for each
pointer move. This rule keeps undo behavior predictable and limits memory use.
Large documents can make full snapshots expensive. In that case, store commands
or patches with enough data to apply and reverse each change. This model is more
complex, so use it only after measurement shows a snapshot limit.
Define a history limit for a long editor session. Remove the oldest snapshots
after the limit is reached. Keep saved document versions separate from the local
undo history.
See the focused React undo and redo example for a
smaller implementation.