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.

Mini Map

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 in workflows representing 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.

Frequently Asked Questions

React Flow Undo Redo: Workflow Editor History (Part 4)