myHotTake

Tag: NgRx tutorial

  • 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.
  • How Do I Manage State in Complex Angular Apps Efficiently?

    If you find this story helpful, feel free to like or share it!


    I’m the manager of a restaurant kitchen. Each dish I prepare is like a component in my Angular application, with its own set of ingredients—just like state in the app. In this kitchen, managing all these ingredients efficiently is crucial to ensuring every dish is perfect and goes out on time.

    In this analogy, the pantry is my centralized store, like NgRx in Angular. It’s where all my ingredients (or state) are stored. Instead of having separate mini-pantries for each dish, I use this central pantry to keep everything organized. This way, I know exactly where to find what I need, just like I would access state from a central store.

    Now, imagine each ingredient has a label that tells me the last time it was used and its quantity. This is like using selectors in Angular, which help me efficiently retrieve only the necessary state without rummaging through the entire pantry. It saves me time and ensures I’m always using fresh ingredients.

    When a new order comes in, I write it down on a ticket—my action. This ticket travels to the head chef, who decides how to adjust the pantry’s inventory. This is akin to reducers in Angular, which handle actions to update the state. By having this process in place, I maintain consistency and ensure no ingredient is overused or forgotten.

    Sometimes, I have to make special sauces or desserts that require complex preparation. For these, I set up a separate workstation, much like using services in Angular to manage complicated state logic. This keeps my main cooking area clear and focused on the core dishes, ensuring efficiency and clarity.

    In my kitchen, communication is key. Just as I rely on my team to know when to prepare appetizers or desserts, in Angular, I use effects to handle asynchronous operations, like fetching data from the server. This way, the main cooking line isn’t held up, and everything gets done smoothly and in sync.

    By managing the kitchen this way, I ensure that every dish is a success, just as effectively managing state in Angular results in a smooth, responsive application. So, if you find this helpful, remember to give a like or share!


    Centralized Store (The Pantry)

    In Angular, we often use NgRx to manage the centralized state. Here’s how we might set up a simple store:

    // app.state.ts
    export interface AppState {
      ingredients: string[];
    }
    
    // initial-state.ts
    export const initialState: AppState = {
      ingredients: []
    };

    Actions (The Order Tickets)

    Actions in NgRx are like the order tickets in the kitchen. They specify what needs to be done:

    // ingredient.actions.ts
    import { createAction, props } from '@ngrx/store';
    
    export const addIngredient = createAction(
      '[Ingredient] Add Ingredient',
      props<{ ingredient: string }>()
    );

    Reducers (The Head Chef’s Decisions)

    Reducers handle the actions and update the state accordingly:

    // ingredient.reducer.ts
    import { createReducer, on } from '@ngrx/store';
    import { addIngredient } from './ingredient.actions';
    import { initialState } from './initial-state';
    
    const _ingredientReducer = createReducer(
      initialState,
      on(addIngredient, (state, { ingredient }) => ({
        ...state,
        ingredients: [...state.ingredients, ingredient]
      }))
    );
    
    export function ingredientReducer(state, action) {
      return _ingredientReducer(state, action);
    }

    Selectors (Labeling the Ingredients)

    Selectors help fetch specific parts of the state efficiently:

    // ingredient.selectors.ts
    import { createSelector } from '@ngrx/store';
    
    export const selectIngredients = (state: AppState) => state.ingredients;

    Services and Effects (Special Workstations)

    Services or effects handle complex logic, like fetching data:

    // ingredient.effects.ts
    import { Injectable } from '@angular/core';
    import { Actions, ofType, createEffect } from '@ngrx/effects';
    import { addIngredient } from './ingredient.actions';
    import { tap } from 'rxjs/operators';
    
    @Injectable()
    export class IngredientEffects {
      addIngredient$ = createEffect(
        () =>
          this.actions$.pipe(
            ofType(addIngredient),
            tap(action => {
              console.log(`Added ingredient: ${action.ingredient}`);
            })
          ),
        { dispatch: false }
      );
    
      constructor(private actions$: Actions) {}
    }

    Key Takeaways

    1. Centralized State Management: Like a well-organized pantry, a centralized store helps manage state efficiently across the application.
    2. Actions and Reducers: Actions act as orders for change, while reducers decide how to execute those changes, ensuring consistency.
    3. Selectors: These help retrieve only the necessary state, improving performance and maintainability.
    4. Effects and Services: Manage complex logic and asynchronous operations without cluttering the main state management logic.

    By managing state in this structured way, we ensure that our Angular application runs smoothly, much like a well-coordinated kitchen ensures timely and perfect dishes. I hope this sheds light on how to manage state effectively in Angular!