myHotTake

Tag: Manage Angular state

  • Services vs. NgRx: Which Should You Choose for State?

    If this story makes state management finally click, feel free to give it a like or share—it might help someone else out too!


    Think of state management like running a daycare for kids’ toys. Stick with me.

    When I started, I ran my daycare the simple way—just me and my backpack. Every kid brought me their toys to hold, and when they wanted something back, I’d dig around to find it. This worked when there were only a handful of kids. In JavaScript terms, that’s like using services for state management: straightforward, fast to set up, and great when the toy count is low.

    But then, the daycare grew. More kids. More toys. Suddenly, the backpack was overflowing. Toys were getting lost, and the kids started fighting about who had what. It was chaos! That’s when I decided to implement a “toy cabinet system,” color-coded and labeled, where every kid’s toys were stored in their designated section. Now, whenever a toy was needed, the kids knew exactly where to find it—or I could grab it for them quickly. That’s NgRx: a structured, predictable system for managing complex states.

    The difference is simple. If my daycare is small, I don’t need a fancy cabinet; my backpack works fine. But as the daycare scales, the cabinet ensures everything is organized, and I don’t lose my mind.

    So, when choosing between services and NgRx, I ask myself: how many toys am I managing, and how much chaos can I handle?


    Using Services (The Backpack)

    If you have a simple app, a service can manage your state just fine. Let’s say you’re tracking a single list of toys:

    @Injectable({
      providedIn: 'root',
    })
    export class ToyService {
      private toys: string[] = [];
    
      getToys(): string[] {
        return this.toys;
      }
    
      addToy(toy: string): void {
        this.toys.push(toy);
      }
    }

    You can inject this ToyService into components, call addToy to update the state, and getToys to retrieve it. Easy, lightweight, and minimal setup.

    Using NgRx (The Toy Cabinet)

    When the daycare (or app) grows, you might have multiple states to track: toys, snack schedules, attendance, etc. Here’s how you’d manage the toy state with NgRx:

    Define Actions:

    export const addToy = createAction('[Toy] Add', props<{ toy: string }>());
    export const loadToys = createAction('[Toy] Load');

    Define a Reducer:

    export interface ToyState {
      toys: string[];
    }
    
    const initialState: ToyState = {
      toys: [],
    };
    
    export const toyReducer = createReducer(
      initialState,
      on(addToy, (state, { toy }) => ({
        ...state,
        toys: [...state.toys, toy],
      }))
    );

    Set Up a Selector:

    export const selectToys = (state: { toys: ToyState }) => state.toys.toys;

    Use It in a Component:

    @Component({
      selector: 'app-toy-list',
      template: `
        <ul>
          <li *ngFor="let toy of toys$ | async">{{ toy }}</li>
        </ul>
        <button (click)="addToy('Teddy Bear')">Add Toy</button>
      `,
    })
    export class ToyListComponent {
      toys$ = this.store.select(selectToys);
    
      constructor(private store: Store) {}
    
      addToy(toy: string): void {
        this.store.dispatch(addToy({ toy }));
      }
    }

    With NgRx, everything is organized: actions define what can happen, reducers update the state, and selectors make it easy to fetch data. The toy cabinet is fully in place.


    Key Takeaways

    1. Services (Backpack): Great for small apps with simple state. Quick, lightweight, and easy to implement.
    • Use services when managing a single slice of state or when you don’t need global state synchronization.
    1. NgRx (Toy Cabinet): Ideal for complex apps where multiple states interact or need to be shared.
    • Use NgRx when your app scales and you need predictability, immutability, and testability.
    1. Transition Wisely: Start with a backpack (services) and upgrade to a toy cabinet (NgRx) as your app grows.

    By matching the tool to the scale of your app, you’ll keep things simple and efficient—just like a well-run daycare. 🎒 vs. 🗄️

  • 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.