5 April 2026
React Flow Undo Redo: Workflow Editor History (Part 4)
This is the fourth part of a series on building a workflow editor on top of React Flow.
In this part, you'll learn how to add undo and redo to your workflow editor by keeping a full record of past and future states, rather than just the current one.
The Problem with Simple State
In the previous parts, we managed workflow state with a simple useState.
export default function Home() {
const [workflow, setWorkflow] = useState(() => initialWorkflow());
const onWorkflowChange = useCallback((changes: WorkflowChange[]) => {
setWorkflow((workflow) => applyWorkflowChanges(changes, workflow));
}, []);
return (
<div className="flex h-screen flex-col bg-neutral-900">
<WorkflowView workflow={workflow} onWorkflowChange={onWorkflowChange} />
</div>
);
}
This works, but it only holds one state and there's no way to go back.
History Management
The fix is to replace useState with a useWorkflow hook that wraps a reducer and maintains a history.
export default function Home() {
const [state, dispatch] = useWorkflow(workflow);
const onWorkflowChange = useCallback(
(changes: WorkflowChange[]) => {
dispatch({ type: "applyWorkflowChanges", changes });
},
[dispatch],
);
return (
<div className="flex h-screen flex-col bg-neutral-900">
<Topbar state={state} dispatch={dispatch} />
<div className="relative grow">
<WorkflowView
workflow={state.current}
onWorkflowChange={onWorkflowChange}
/>
</div>
</div>
);
}
This hook returns an object with the following properties:
workflows: The workflow states used for undo/redo, including past and future states.pointer: The index inworkflowsrepresenting the current position in the history.current: The currently active workflow state.
The reducer handles both undo/redo logic and the application of workflow changes.
function reducer(state: State, action: Action): State {
switch (action.type) {
case "applyWorkflowChanges": {
// ...
}
case "navigateBack": {
// ...
}
case "navigateNext": {
// ...
}
}
}
function init(workflow: Workflow): State {
return {
workflows: [workflow],
pointer: 0,
current: workflow,
};
}
export function useWorkflow(workflow: Workflow): [State, Dispatch<Action>] {
return useReducer(reducer, workflow, init);
}
Not Every Change Belongs in History
The undo and redo system should only capture meaningful changes. Dragging a node or selecting it are actions users don't expect to undo, so recording them would not make any sense.
To handle this, we can make applyWorkflowChange return a memory boolean to tell whether to record the change in the history or simply update the current state in place, as shown below.
function reducer(state: State, action: Action): State {
switch (action.type) {
case "applyWorkflowChanges": {
const workflows: Workflow[] = [...state.workflows];
let current: Workflow = state.current;
let pointer: number = state.pointer;
for (const change of action.changes) {
const output = applyWorkflowChange(change, current);
current = output.workflow;
if (output.memory) {
pointer++;
workflows.splice(pointer, workflows.length - pointer, {
...output.workflow,
widget: null,
active: null,
});
}
}
return { workflows: workflows, pointer, current };
}
// ...
}
}
// ...
Next Steps
You now have a solid understanding of the core concepts behind building a workflow editor in React Flow. If you want get access to a fully working implementation with everything covered you can go here.