Building It Step By StepUndo and Redo System

Building It Step By Step

Undo and Redo System

Learn how to add undo and redo capabilities to navigate between previous and future states.


Getting Started

Navigate to the corresponding folder:

cd step-6-undo-and-redo

Install the dependencies:

npm install

Then run the project:

npm run dev

Undo and Redo

The undo and redo functionality is implemented in the useWorkflow hook.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function reducer(state: State, action: Action): State {
  // ...
}

function init(workflow: Workflow): State {
  return {
    workflows: [workflow],
    pointer: 0,
    current: workflow,
  };
}

export function useWorkflow(schema: WorkflowSchema): [State, Dispatch<Action>] {
  return useReducer(reducer, schema, init);
}

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.

When applying a change, applyWorkflowChange returns a memory property that defines whether that change has to be saved in the history, as shown here.

// src/shared/kits/workflow/hooks/use-workflow.ts

// ...

function reducer(state: State, action: Action): State {
  switch (action.type) {
    /**
     * Applies all incoming workflow changes. Each change may request the resulting
     * workflow to be saved in the history (`memory`), in which case a new entry
     * is added and any forward history (redo states) is discarded.
     */
    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 };
    }

    // ...
  }
}

// ...

This is because not all changes, such as selecting or dragging nodes, need to be saved.