myHotTake

Tag: State management tips

  • Avoiding Common NgRx Mistakes: A Chef’s Guide to Success

    If you find this story helpful, feel free to like or share it with others who might benefit too!


    I’m a chef in a kitchen, and my task is to prepare a complex dish using NgRx as my recipe guide. NgRx is like a sophisticated cookbook that helps me manage the chaos of orders and ingredients flowing through the kitchen.

    One common mistake I might make as a chef is trying to cook every dish myself, similar to how I might mistakenly handle too many side effects directly within my components instead of using effects for managing those side effects. It’s like attempting to juggle all the pans at once, rather than letting my sous-chefs (effects) handle specific tasks like fetching ingredients (API calls) or notifying the waitstaff (updating the UI).

    Another pitfall in my culinary journey is not organizing my pantry, which mirrors how I might mismanage the state structure. If I don’t categorize my ingredients (state) properly, I end up wasting time searching for what I need. In NgRx, I should ensure my state is well-organized and normalized, so I can quickly access the data, just like having a neat pantry where everything has its place.

    Then there’s the temptation to overcomplicate the recipe. I might add too many garnishes or unnecessary steps, akin to over-engineering selectors or actions in NgRx. Instead, I should keep things simple and focused, ensuring each action and selector has a clear purpose, just like each ingredient should enhance the dish without overwhelming it.

    Lastly, I might forget to taste as I go, which is equivalent to neglecting to test my NgRx setup. I need to continually sample the dish (test my code) to ensure it’s coming together as expected, allowing me to catch any mistakes early rather than after it’s served.

    NgRx, much like a great recipe, requires balance, organization, and constant attention to detail. By avoiding these common mistakes, I can create a seamless and efficient kitchen, where every dish is a success.


    Continuing my role as a chef, I realize that each section of the kitchen (or each part of my NgRx setup) has a specific function. Let’s explore how I can translate my cooking experience into code.

    1. Delegating Tasks to Sous-Chefs (Effects): In the kitchen, I delegate tasks to my sous-chefs. Similarly, in NgRx, I use effects to handle side effects. Here’s a code snippet:
       import { Injectable } from '@angular/core';
       import { Actions, createEffect, ofType } from '@ngrx/effects';
       import { map, mergeMap } from 'rxjs/operators';
       import { MyService } from './my-service';
       import * as MyActions from './my.actions';
    
       @Injectable()
       export class MyEffects {
    
         constructor(
           private actions$: Actions,
           private myService: MyService
         ) {}
    
         loadItems$ = createEffect(() => this.actions$.pipe(
           ofType(MyActions.loadItems),
           mergeMap(() => this.myService.getItems()
             .pipe(
               map(items => MyActions.loadItemsSuccess({ items }))
             ))
         ));
       }

    Here, the effect loadItems$ listens for loadItems actions and delegates the task of fetching items to the myService.

    1. Organizing the Pantry (State Structure): Just as I organize my pantry, I need to structure my state clearly. Here’s an example of a well-structured state:
       export interface AppState {
         products: ProductState;
         users: UserState;
       }
    
       export interface ProductState {
         items: Product[];
         loading: boolean;
       }
    
       export interface UserState {
         profile: UserProfile;
         isLoggedIn: boolean;
       }

    This structure allows me to easily access and manage different parts of the state, just like finding ingredients quickly in a well-organized pantry.

    1. Keeping the Recipe Simple (Selectors and Actions): I avoid overcomplicating my dishes by keeping my actions and selectors simple:
       // Actions
       export const loadItems = createAction('[Product] Load Items');
       export const loadItemsSuccess = createAction('[Product] Load Items Success', props<{ items: Product[] }>());
    
       // Selectors
       import { createSelector, createFeatureSelector } from '@ngrx/store';
    
       export const selectProductState = createFeatureSelector<ProductState>('products');
    
       export const selectAllItems = createSelector(
         selectProductState,
         (state: ProductState) => state.items
       );

    Each action and selector has a clear and specific purpose, much like each ingredient in a dish.

    1. Tasting as I Cook (Testing): Finally, I ensure everything tastes right by testing my NgRx setup:
       it('should load items successfully', () => {
         const { result } = reducer(initialState, loadItemsSuccess({ items: mockItems }));
         expect(result.items).toEqual(mockItems);
       });

    This test ensures that my actions and reducers work as expected, just like tasting my dish to avoid surprises.

    Key Takeaways:

    • Use Effects Wisely: Delegate side effects to effects to keep components clean and focused.
    • Organize State Structure: A well-structured state is like an organized pantry, making data access efficient.
    • Simplicity is Key: Keep actions and selectors straightforward to avoid over-engineering.
    • Test Regularly: Testing ensures your NgRx setup works as intended, similar to tasting a dish throughout its preparation.
  • 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.