myHotTake

Tag: NgRx state management

  • How to Manage State in Lazy-Loaded Angular Modules with NgRx?

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


    So, picture this: I’m the proud owner of a estate, and each room in my mansion represents a different feature of an app. Now, my mansion is so large that I don’t want to keep every room fully furnished all the time, because it’s just too much to manage. That’s where the concept of lazy-loading comes in. I only bring out the furniture for each room when I’m ready to entertain guests in that particular area.

    Now, state management is like the butler of my mansion. This butler is responsible for keeping track of everything—where the furniture is, what needs to be cleaned, and who’s visiting which room. In my case, this butler is NgRx. He makes sure that every time I open a room, the right furniture and decor are set up just as I like them.

    But here’s the twist: each room might have its unique style and requirements, so my butler has a special way of managing this. When I decide I want to open a room (or in app terms, a feature module), the butler doesn’t rush to set everything up at once. Instead, he waits for the cue—like when a guest walks into the room. This is the lazy-loading part.

    Once the guest enters, the butler quickly and efficiently sets up the room’s state, pulling out the right chairs, tables, and decorations from storage (the store) and placing them where they need to be. This way, I ensure that my estate runs smoothly and efficiently, without wasting resources on rooms that aren’t in use.

    And just like that, I maintain a well-organized and efficient mansion, with the butler—my trusty NgRx—handling the state of each room seamlessly, ensuring that everything is in place exactly when it’s needed. So, in the world of my app mansion, lazy-loaded modules and NgRx work together like a charm to create a harmonious and efficient living space.


    In our mansion, the rooms are the lazy-loaded modules. In JavaScript, we achieve this by using Angular’s loadChildren property in the routing configuration to load these modules only when needed. Here’s an example:

    const routes: Routes = [
      {
        path: 'feature',
        loadChildren: () =>
          import('./feature/feature.module').then((m) => m.FeatureModule),
      },
    ];

    This is like deciding which room to open for the guests, ensuring that the room is only set up when someone enters.

    Now, the butler, who manages everything, is represented by the NgRx store. For each lazy-loaded module, we create a feature state. This can be done by defining feature-specific actions, reducers, and selectors. When a module is loaded, we dynamically register its reducer with the store. Here’s how:

    1. Define Actions and Reducer for the feature module:
       import { createAction, createReducer, on, props } from '@ngrx/store';
    
       export const loadFeatureData = createAction('[Feature] Load Data');
       export const loadFeatureDataSuccess = createAction(
         '[Feature] Load Data Success',
         props<{ data: any }>()
       );
    
       export interface FeatureState {
         data: any;
       }
    
       const initialState: FeatureState = {
         data: null,
       };
    
       const featureReducer = createReducer(
         initialState,
         on(loadFeatureDataSuccess, (state, { data }) => ({ ...state, data }))
       );
    
       export function reducer(state: FeatureState | undefined, action: Action) {
         return featureReducer(state, action);
       }
    1. Register the Reducer when the module is loaded:
       import { NgModule } from '@angular/core';
       import { StoreModule } from '@ngrx/store';
       import { reducer } from './feature.reducer';
    
       @NgModule({
         imports: [
           StoreModule.forFeature('feature', reducer), // Register the feature state
           // other imports
         ],
         // declarations, providers, etc.
       })
       export class FeatureModule {}

    This is akin to the butler setting up the room’s state—bringing out the right furniture and decor when the room is used.

    1. Use Selectors to access the feature state:
       import { createFeatureSelector, createSelector } from '@ngrx/store';
       import { FeatureState } from './feature.reducer';
    
       export const selectFeatureState = createFeatureSelector<FeatureState>('feature');
    
       export const selectFeatureData = createSelector(
         selectFeatureState,
         (state) => state.data
       );

    By using selectors, we can efficiently access the state to ensure everything is in place, just like how the butler knows exactly where each piece of furniture belongs.


    Key Takeaways:

    • Lazy Loading with Angular: Use loadChildren to load feature modules only when needed, optimizing resource usage.
    • Dynamic State Management with NgRx: Register feature-specific reducers when modules are loaded to manage state dynamically.
    • Selectors for State Access: Utilize selectors to efficiently retrieve and manage state within modules, ensuring a seamless user experience.

    In conclusion, managing state in lazy-loaded modules with NgRx is like efficiently running a mansion with a skilled butler, ensuring everything is perfectly in place when needed without unnecessary resource usage. This approach helps in building scalable and performant Angular applications.

  • NgRx Async Updates Explained: What’s the Process?

    If you find this explanation helpful or just love creative tech analogies, feel free to share or drop a like! Let’s dive in.


    I’m running a typical coffee shop, and my specialty is making complex, custom coffee orders. Now, orders come flying in all day from all over—online, walk-ins, drive-thru. My baristas, bless them, can’t keep up in real-time. So, I set up a genius solution: a chalkboard order board that everyone can see.

    Each time someone places an order, it’s added to the chalkboard. That’s like dispatching an action in NgRx. It’s just the order—a clear declaration of what someone wants. No coffee’s made yet; we just know what needs to happen.

    Now, I have a highly focused team member—a “state manager.” Their job is to see every order on the board and update the status board where we track all orders—what’s being made, what’s ready, and what’s delivered. This status board is our store—the single source of truth that anyone in the shop can check to know what’s going on.

    But here’s where it gets cool. Some orders, like fancy espresso drinks, take longer than others. Instead of waiting for each drink to finish before updating the status board (that would cause chaos!), my state manager uses a timer to handle asynchronous updates. When an espresso order is complete, the machine pings them, and they update the board without missing a beat. That’s where effects come in—they handle side tasks like brewing coffee while the state stays neat and tidy.

    With this system, no one’s overwhelmed, and no order is lost. I can confidently handle hundreds of orders a day without missing a beat. NgRx, like my coffee shop, thrives because the state is centralized, asynchronous tasks are offloaded, and everyone knows exactly where to look to find the truth about any order.


    The Order (Actions)

    Actions in NgRx represent those chalkboard orders—what we want to happen. Let’s define an action for placing a coffee order:

    import { createAction, props } from '@ngrx/store';
    
    // Customer places an order
    export const placeOrder = createAction(
      '[Coffee Shop] Place Order',
      props<{ orderId: string; drink: string }>()
    );

    This placeOrder action tells the system that a new drink order is in. Just like adding to the chalkboard, this doesn’t brew the coffee yet—it just signals intent.


    The Status Board (State and Reducers)

    The state is our single source of truth, tracking every order’s status. The reducer updates this state based on the actions:

    import { createReducer, on } from '@ngrx/store';
    import { placeOrder } from './coffee.actions';
    
    // Initial state for the coffee shop
    export interface CoffeeState {
      orders: { [key: string]: string }; // Mapping orderId to its status
    }
    
    export const initialState: CoffeeState = {
      orders: {}
    };
    
    export const coffeeReducer = createReducer(
      initialState,
      // Update state when an order is placed
      on(placeOrder, (state, { orderId, drink }) => ({
        ...state,
        orders: { ...state.orders, [orderId]: `Preparing ${drink}` }
      }))
    );

    Now, whenever placeOrder is dispatched, the reducer updates the status board (our state).


    The Barista (Effects for Async Tasks)

    Not every drink is instant—our espresso machine needs time. Effects manage these asynchronous tasks.

    import { createEffect, ofType, Actions } from '@ngrx/effects';
    import { Injectable } from '@angular/core';
    import { of } from 'rxjs';
    import { delay, map } from 'rxjs/operators';
    import { placeOrder } from './coffee.actions';
    
    @Injectable()
    export class CoffeeEffects {
      constructor(private actions$: Actions) {}
    
      makeCoffee$ = createEffect(() =>
        this.actions$.pipe(
          ofType(placeOrder),
          delay(3000), // Simulate brewing time
          map(({ orderId }) => ({
            type: '[Coffee Shop] Order Ready',
            orderId
          }))
        )
      );
    }

    Here, the effect listens for placeOrder, simulates a 3-second brewing delay, and then dispatches a new action when the coffee is ready.


    Final Touches: Selectors and Key Takeaways

    Selectors allow us to fetch specific data from the state, like the status of a specific order:

    import { createSelector } from '@ngrx/store';
    
    export const selectOrders = (state: CoffeeState) => state.orders;
    
    export const selectOrderStatus = (orderId: string) =>
      createSelector(selectOrders, (orders) => orders[orderId]);

    With selectors, components can stay focused on just the information they need, keeping everything efficient and clean.


    Key Takeaways

    1. Actions are the chalkboard orders: they declare intent but don’t execute logic.
    2. Reducers update the state (status board) based on those actions.
    3. Effects handle asynchronous processes (like brewing coffee) without clogging up the main flow.
    4. Selectors fetch just the right slice of state for a given purpose.
  • How to Manage Global State in Angular Apps Effectively

    If you find this helpful, feel free to like or share! Let’s dive into the story. 🌟


    I’m running a relatively big city with many neighborhoods, and each neighborhood has its own mayor for some reason. The mayors are great at managing local issues, like fixing potholes or organizing block parties. But what happens when there’s a citywide event, like a power outage or a new traffic rule? That’s when I need a City Hall—one central place to make decisions that affect everyone.

    In my Angular application, managing global state is like running this city. Each neighborhood is a component, perfectly capable of handling its own small state. But when I have something important that multiple neighborhoods need to know—like the weather forecast or city budget—I rely on City Hall. This City Hall is my state management solution, like NgRx or BehaviorSubject, acting as a centralized store.

    When a neighborhood (component) needs to know the weather, it doesn’t call up every other neighborhood; it checks with City Hall. And if there’s an update—like a sudden storm—City Hall announces it to all the neighborhoods at once. Everyone stays in sync without endless chatter between the neighborhoods.

    The trick is to keep City Hall efficient. I don’t want it bogged down with every tiny detail. So, I only store what’s essential: citywide events, rules, or shared data. Local stuff? That stays with the mayors, or the components.

    This balance between local mayors and City Hall keeps the city—er, the app—running smoothly, even as it grows bigger. And that’s how I manage global state in Angular!


    Example 1: Using a Service with BehaviorSubject

    The BehaviorSubject acts as City Hall, storing and broadcasting state updates.

    // city-hall.service.ts
    import { Injectable } from '@angular/core';
    import { BehaviorSubject } from 'rxjs';
    
    @Injectable({
      providedIn: 'root',
    })
    export class CityHallService {
      private weatherSource = new BehaviorSubject<string>('Sunny');
      public weather$ = this.weatherSource.asObservable();
    
      setWeather(newWeather: string): void {
        this.weatherSource.next(newWeather); // Announce new weather to all components
      }
    }

    Here, the weatherSource is the centralized state. Components subscribe to weather$ to stay informed.


    Example 2: Components as Neighborhoods

    Let’s see how neighborhoods (components) interact with City Hall.

    WeatherDisplayComponent:

    import { Component, OnInit } from '@angular/core';
    import { CityHallService } from './city-hall.service';
    
    @Component({
      selector: 'app-weather-display',
      template: `<p>The weather is: {{ weather }}</p>`,
    })
    export class WeatherDisplayComponent implements OnInit {
      weather: string = '';
    
      constructor(private cityHallService: CityHallService) {}
    
      ngOnInit(): void {
        this.cityHallService.weather$.subscribe((weather) => {
          this.weather = weather; // Stay updated with City Hall's announcements
        });
      }
    }

    WeatherControlComponent:

    import { Component } from '@angular/core';
    import { CityHallService } from './city-hall.service';
    
    @Component({
      selector: 'app-weather-control',
      template: `<button (click)="changeWeather()">Change Weather</button>`,
    })
    export class WeatherControlComponent {
      constructor(private cityHallService: CityHallService) {}
    
      changeWeather(): void {
        const newWeather = prompt('Enter new weather:');
        if (newWeather) {
          this.cityHallService.setWeather(newWeather); // Update City Hall
        }
      }
    }

    Example 3: NgRx for Large-Scale State

    For a bigger city (application), I might use NgRx for even more structured state management.

    Defining the State:

    // weather.state.ts
    export interface WeatherState {
      weather: string;
    }
    
    export const initialState: WeatherState = {
      weather: 'Sunny',
    };

    Action:

    // weather.actions.ts
    import { createAction, props } from '@ngrx/store';
    
    export const setWeather = createAction(
      '[Weather] Set Weather',
      props<{ weather: string }>()
    );

    Reducer:

    // weather.reducer.ts
    import { createReducer, on } from '@ngrx/store';
    import { setWeather } from './weather.actions';
    import { WeatherState, initialState } from './weather.state';
    
    export const weatherReducer = createReducer(
      initialState,
      on(setWeather, (state, { weather }) => ({ ...state, weather }))
    );

    Selectors:

    // weather.selectors.ts
    import { createSelector, createFeatureSelector } from '@ngrx/store';
    import { WeatherState } from './weather.state';
    
    export const selectWeatherState = createFeatureSelector<WeatherState>('weather');
    export const selectWeather = createSelector(
      selectWeatherState,
      (state: WeatherState) => state.weather
    );

    Component Using NgRx Store:

    // weather-display.component.ts
    import { Component, OnInit } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { Observable } from 'rxjs';
    import { selectWeather } from './weather.selectors';
    
    @Component({
      selector: 'app-weather-display',
      template: `<p>The weather is: {{ weather$ | async }}</p>`,
    })
    export class WeatherDisplayComponent {
      weather$: Observable<string>;
    
      constructor(private store: Store) {
        this.weather$ = this.store.select(selectWeather);
      }
    }

    Key Takeaways

    1. Choose the Right Tool: For small apps, a service with BehaviorSubject is lightweight and effective. For larger apps, NgRx offers robust state management.
    2. Centralize Shared State: Use a service or store as a single source of truth (City Hall) for global data, while keeping local state within components.
    3. Keep it Clean: Don’t overload your centralized state with unnecessary data. Balance local and global state effectively.
  • What Is NgRx and Why Use It for Angular State Management?

    If this story helps make NgRx clear and simple, feel free to like or share—it might just help someone else, too!


    I’m running a farm with a bunch of fields, and each field is growing a different crop. I’ve got wheat in one, corn in another, and a little pond for fish. It’s a lot to manage, so I hire a farm manager—that’s NgRx in this analogy.

    Now, my job is to tell the farm manager what needs to happen. If I need the corn watered, I don’t personally run out with a watering can—I write a note saying “Water the corn” and hand it to the manager. That note? That’s an action in NgRx. It’s just a description of what I want to happen.

    The farm manager doesn’t water the corn directly. Instead, they look at the farm’s playbook—the reducer—which tells them how to update the field based on the action. The reducer doesn’t water the corn itself, either—it just updates the farm’s ledger, called the store, saying, “Corn was watered.” This ledger keeps track of the entire farm’s state: what’s planted, what’s watered, what’s harvested.

    But here’s the magical part. Every field worker on the farm who’s keeping an eye on the ledger immediately knows when something changes. That’s because of selectors—they’re like workers who only pay attention to the specific parts of the ledger they care about. So, if I’ve got a chef waiting for fresh corn, they’ll know instantly when the corn is ready to pick.

    NgRx is like this efficient system where I don’t have to run around the farm myself. It keeps everything organized, predictable, and ensures everyone is on the same page about what’s happening. Without it, managing the farm—or a complex Angular app—would be chaos.


    1. Actions: The Notes You Hand to the Manager

    An action is just a JavaScript object with a type and optionally some payload. Here’s how we write one:

    import { createAction, props } from '@ngrx/store';
    
    export const waterCorn = createAction(
      '[Farm] Water Corn', 
      props<{ amount: number }>() // Payload to specify the amount of water
    );

    This waterCorn action describes what needs to happen but doesn’t do the work itself.


    2. Reducers: The Playbook for the Manager

    The reducer is a pure function that knows how to update the state (the farm’s ledger) based on the action it receives.

    import { createReducer, on } from '@ngrx/store';
    import { waterCorn } from './farm.actions';
    
    export interface FarmState {
      cornWatered: number;
    }
    
    const initialState: FarmState = {
      cornWatered: 0,
    };
    
    export const farmReducer = createReducer(
      initialState,
      on(waterCorn, (state, { amount }) => ({
        ...state,
        cornWatered: state.cornWatered + amount,
      }))
    );

    This reducer listens for the waterCorn action and updates the state.


    3. Selectors: The Workers Who Watch the Ledger

    Selectors are functions that extract specific pieces of state. This is how we get data like the current amount of water the corn has received.

    import { createSelector } from '@ngrx/store';
    
    export const selectFarmState = (state: any) => state.farm;
    
    export const selectCornWatered = createSelector(
      selectFarmState,
      (farmState) => farmState.cornWatered
    );

    By using selectors, Angular components don’t need to worry about the whole ledger—they only get the part they need.


    4. Dispatching Actions: Handing the Note

    In Angular, we dispatch actions using the Store service. This is like handing a note to the farm manager.

    import { Component } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { waterCorn } from './farm.actions';
    
    @Component({
      selector: 'app-farm',
      template: `<button (click)="water()">Water Corn</button>`,
    })
    export class FarmComponent {
      constructor(private store: Store) {}
    
      water() {
        this.store.dispatch(waterCorn({ amount: 5 }));
      }
    }

    5. Subscribing to State Changes: Workers Watching the Fields

    Components can subscribe to selectors to react to changes in the state.

    import { Component } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { selectCornWatered } from './farm.selectors';
    
    @Component({
      selector: 'app-farm-status',
      template: `<p>Corn watered: {{ cornWatered$ | async }}</p>`,
    })
    export class FarmStatusComponent {
      cornWatered$ = this.store.select(selectCornWatered);
    }

    The cornWatered$ observable automatically updates when the state changes.


    Key Takeaways / Final Thoughts

    • Predictability: The reducer ensures state changes are consistent and predictable.
    • Centralized State: The store is a single source of truth for your application.
    • Reactivity: Selectors and observables make it easy for components to react to changes in the state.
    • Scalability: As the app grows, NgRx keeps state management clean and maintainable.

    NgRx might seem complex at first, but it provides structure to manage state effectively, especially in large Angular applications. With actions, reducers, selectors, and the store, it turns chaos into a well-oiled machine.