myHotTake

Tag: Angular state control

  • How to Implement Undo/Redo in NgRx: A Step-by-Step Guide

    If you find this story helpful, feel free to give it a like or share it with others who might enjoy it!


    I’m a painter with a magical canvas. Every brushstroke I make is like dispatching an action in NgRx, adding a new layer to my artwork. Sometimes, I step back and realize that I preferred how the painting looked a few strokes ago. Thankfully, my canvas has an enchanted feature: an undo/redo button.

    In this world of painting, my canvas has two stacks: one for undo and one for redo. Every time I make a change—a brushstroke, a splash of color—it adds the current state of the painting to the undo stack. This is like how NgRx stores past states when actions are dispatched.

    Now, let’s say I want to undo a stroke. I press the undo button, and the canvas takes the most recent state from the undo stack and applies it, moving the current state to the redo stack. It’s like rewinding time, and I can see the painting as it was before that last brushstroke. In NgRx, this is akin to reverting back to a previous state.

    But what if I realize that I want that stroke back after all? The redo button is there to rescue me. Pressing redo retrieves the last state stored in the redo stack and applies it back to the canvas, shifting it back to the undo stack. In NgRx terms, this is like reapplying an action that has been undone.

    This magical canvas ensures I can explore my creativity freely, knowing that every change is reversible. Just like with NgRx, having undo/redo functionality gives me the confidence to experiment, knowing I can always go back or move forward. My painting evolves, but I never lose sight of the past or the potential of the future.


    Part 2: From Canvas to Code

    In the world of NgRx, implementing undo/redo functionality involves managing state changes using actions and reducers. Here’s a simplified overview of how we can mirror the painter’s magical canvas in code.

    Setting Up the State

    First, we define a state that holds two stacks: undoStack and redoStack, along with the current state of our application:

    interface AppState {
      currentState: any; // The current state of the application
      undoStack: any[];  // Stack to keep past states for undo
      redoStack: any[];  // Stack to keep states for redo
    }

    Actions for the Brushstrokes

    Next, we define actions for making changes (brushstrokes) and for undoing/redoing:

    import { createAction, props } from '@ngrx/store';
    
    export const makeChange = createAction('[Canvas] Make Change', props<{ newState: any }>());
    export const undo = createAction('[Canvas] Undo');
    export const redo = createAction('[Canvas] Redo');

    Reducer to Handle Actions

    The reducer function manages the state transitions based on the dispatched actions:

    import { createReducer, on } from '@ngrx/store';
    import { makeChange, undo, redo } from './actions';
    
    const initialState: AppState = {
      currentState: null,
      undoStack: [],
      redoStack: []
    };
    
    const canvasReducer = createReducer(
      initialState,
      on(makeChange, (state, { newState }) => ({
        currentState: newState,
        undoStack: [...state.undoStack, state.currentState],
        redoStack: [] // Clear redo stack on new changes
      })),
      on(undo, (state) => {
        const previousState = state.undoStack[state.undoStack.length - 1];
        if (previousState !== undefined) {
          return {
            currentState: previousState,
            undoStack: state.undoStack.slice(0, -1),
            redoStack: [state.currentState, ...state.redoStack]
          };
        }
        return state;
      }),
      on(redo, (state) => {
        const nextState = state.redoStack[0];
        if (nextState !== undefined) {
          return {
            currentState: nextState,
            undoStack: [...state.undoStack, state.currentState],
            redoStack: state.redoStack.slice(1)
          };
        }
        return state;
      })
    );
    
    export function reducer(state: AppState | undefined, action: Action) {
      return canvasReducer(state, action);
    }

    Key Takeaways

    1. State Management: The undoStack and redoStack are essential for storing past and future states, allowing us to navigate changes seamlessly.
    2. Actions as Changes: Every change is an action that potentially updates the state, similar to a brushstroke on the canvas.
    3. Reducer Logic: The reducer ensures that state transitions reflect the intended undo/redo behavior by manipulating the stacks accordingly.
    4. Clear Redo on New Change: Any new change clears the redoStack, as the future states are no longer relevant after a new action.