Code with zaheer

Learn to code

Learn More

Latest Blog Posts

Understanding NgRx in Angular 20: From Basics to Your First Store

Angular 20 · NgRx 20 · Signals

A counter is the smallest app that still uses every part of NgRx. This post walks through the real code behind one: actions, a reducer, selectors, a facade, and a zoneless component that reads the count as a signal.

(click)→ dispatch→ reduce→ store→ select→ render

12 min readAngular 20.3@ngrx/store 20.1

Why use a store for a counter?

You could keep the count in a signal inside the component and stop there. That works until a second component needs the same number, or a bug makes you ask why the number changed. NgRx handles both. It keeps app data in one place, called the Store, and only lets that data change through one path that you can log, inspect and replay.

A bank account is a useful picture. You never edit your balance by hand. You hand in a deposit slip, the bank applies its rules, and a new balance goes into the ledger. When you want to know the balance, you ask for it. Each piece of NgRx has a job like that:

PieceFileIts jobIn the bank
Actioncounter.actions.tsSays what happened, like “Increment was clicked”.Deposit slip
Reducercounter.reducer.tsTakes the current state and an action, returns the next state.The bank’s rules
Stateinside the StoreThe current data, { count: 0 }. One copy for the whole app.The ledger
Selectorcounter.selector.tsReads one piece of state and caches the result.Balance inquiry
Facadecounter.facade.tsA small service that hides NgRx behind plain methods.The teller window
Componentapp.ts, app.htmlShows the count and calls the facade on click.You

The architecture in one picture

This is how the files in the project connect. Writes travel down the left side. Reads come back up the right side.

App component app.ts + app.html · shows count() and calls increment(), decrement(), reset() counterFacade.increment() count: Signal<number> CounterFacade counter.facade.ts · the only class that talks to the Store NgRx Store · provideStore() + provideState('counter', counterReducer) store.dispatch(action) store.selectSignal(selectCount) Action counter.actions.ts [Counter] Increment what happened Reducer counter.reducer.ts count: 0 → 1 returns new state State state.counter { count: 1 } one source of truth Selector counter.selector.ts selectCount → 1 reads one value on() returns read by every action + state action stream Redux DevTools provideStoreDevtools · keeps the last 25 actions Effects provideEffects() · empty for now, HTTP calls go here
Figure 1. The component only talks to the facade. The facade dispatches actions in and reads a signal out. Inside the Store, an action goes to the reducer, the reducer produces new state, and the selector reads it. DevTools and Effects see every action.

Two rules keep this predictable. Data moves in one direction only. And only the reducer creates new state. Everything else either asks for a change by dispatching an action, or reads the result through a selector.

Where each file lives

All NgRx code for the counter sits in one folder, with one file per job. The component and config files outside it only needed small edits.

src/app/
├── app.config.ts               registers the Store
├── app.ts                      component, uses the facade
├── app.html                    template, shows count()
├── app.scss                    button styles
└── counterStore/
    ├── counter.actions.ts      what can happen
    ├── counter.reducer.ts      how state changes
    ├── counter.selector.ts     how to read state
    └── counter.facade.ts       simple API for components

Step 1 · Packages

Install NgRx

NgRx releases follow Angular’s major versions, so an Angular 20 app uses NgRx 20. This project adds three packages:

terminal
npm install @ngrx/store@20 @ngrx/effects@20 @ngrx/store-devtools@20
  • @ngrx/store gives you the Store, actions, reducers and selectors.
  • @ngrx/store-devtools connects the Store to the Redux DevTools browser extension.
  • @ngrx/effects runs side effects such as HTTP calls. The counter doesn’t need one yet, but it is registered so it is ready.

Afterwards, package.json has three new lines:

package.json
     "@angular/router": "^20.3.0",
+    "@ngrx/effects": "^20.1.0",
+    "@ngrx/store": "^20.1.0",
+    "@ngrx/store-devtools": "^20.1.0",
     "rxjs": "~7.8.0",
Side note

This commit also adds "cli": { "analytics": false } to angular.json. That only turns off Angular CLI usage analytics, which is what ng analytics disable writes. It isn’t related to NgRx.

Step 2 · Actions

Describe what can happen

An action is a plain object that says something happened. It always has a type string and can carry extra data. createActionGroup creates every action for one feature in a single call.

src/app/counterStore/counter.actions.ts
import { createActionGroup, emptyProps } from '@ngrx/store';

export const counterActions = createActionGroup({
  source: 'Counter',
  events: {
    Increment: emptyProps(),
    Decrement: emptyProps(),
    Reset: emptyProps(),
  },
});

Each event name turns into a function you can call. NgRx builds the type string from the source and the event name:

You writeIncrement: emptyProps()
You callcounterActions.increment()
The Store receives{ type: '[Counter] Increment' }

emptyProps() means these actions carry no extra data. The [Counter] prefix tells you where an action came from when you read the action log later.

Step 3 · Reducer

Decide how the state changes

The reducer is the only code that creates new state. It is a pure function: give it the current state and an action, and it returns the next state. The same input always gives the same output.

src/app/counterStore/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { counterActions } from './counter.actions';

export interface countState {
  count: number;
}

export const initialState: countState = {
  count: 0,
};

export const counterFeatureKey = 'counter';

export const counterReducer = createReducer(
  initialState,
  on(counterActions.increment, (state) => ({
    ...state,
    count: state.count + 1,
  })),

  on(counterActions.decrement, (state) => ({
    ...state,
    count: state.count - 1,
  })),

  on(counterActions.reset, (state) => ({
    ...state,
    count: 0,
  })),
);
  • countState describes the shape of this slice of state.
  • initialState is the value before any action runs, so the count starts at 0.
  • counterFeatureKey is the name of this slice in the global state object, so it lives at state.counter.
  • Each on() pairs an action with a function that returns the next state.
  • ...state copies any other fields. The counter only has one today, but this keeps the reducer correct when you add more.

The most common beginner mistake is changing the state object directly. Compare the two versions:

✗ Changes the old object throws in dev mode

wrong
on(counterActions.increment, (state) => {
  state.count++;
  return state;
}),

✓ Returns a new object what this project does

right
on(counterActions.increment, (state) => ({
  ...state,
  count: state.count + 1,
})),

NgRx notices changes by comparing object references. If you change the old object and return it, the reference stays the same, so selectors assume nothing happened and the screen doesn’t update. In development mode NgRx also freezes the state, so the left version throws an error on the first click.

Step 4 · Selectors

Read exactly what you need

The Store keeps one object for the whole app. After the counter registers, it looks like this:

global state
{
  "counter": {
    "count": 0
  }
}

Selectors are functions that read pieces of that object.

src/app/counterStore/counter.selector.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { counterFeatureKey, countState } from './counter.reducer';

export const selectCounterState = createFeatureSelector<countState>(counterFeatureKey);

export const selectCount = createSelector(selectCounterState, (state: countState) => state.count);
  • createFeatureSelector picks the counter slice out of the global state.
  • createSelector takes that slice and returns just the number.
Good to know

Selectors are memoized. If the counter slice is the same object as last time, selectCount returns its cached result without running again. That keeps reads cheap, even when many components use them.

Step 5 · Configuration

Register the Store in app.config.ts

This project is a standalone Angular app with no AppModule, so NgRx is registered with provider functions in app.config.ts.

src/app/app.config.ts
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, isDevMode } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideState, provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { provideEffects } from '@ngrx/effects';
import { counterFeatureKey, counterReducer } from './counterStore/counter.reducer';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZonelessChangeDetection(),
    provideRouter(routes),
    provideStore(),
    provideState(counterFeatureKey, counterReducer),
    provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() }),
    provideEffects(),
  ],
};
ProviderWhat it does
provideStore()Creates the global Store. It starts empty, and features add themselves.
provideState(counterFeatureKey, counterReducer)Adds the counter slice at state.counter.
provideStoreDevtools({ maxAge: 25, logOnly: !isDevMode() })Connects to Redux DevTools and keeps the last 25 actions. Watch-only in production builds.
provideEffects()Starts the effects system. There are no effects yet.
provideZonelessChangeDetection()Already in the project. Runs Angular without zone.js, so signals tell Angular when to re-render.
Why a constant

The feature key is used in two places: provideState here and createFeatureSelector in the selector file. Both read the counterFeatureKey constant, so a typo can’t make them disagree.

Step 6 · Facade

Hide the Store behind a facade

A facade is a normal Angular service that sits between components and the Store. Components call its methods and read its signals. They never import actions, selectors or the Store itself.

src/app/counterStore/counter.facade.ts
import { Injectable, inject } from '@angular/core';
import { Store } from '@ngrx/store';

import { counterActions } from './counter.actions';
import { selectCount } from './counter.selector';

@Injectable({ providedIn: 'root' })
export class CounterFacade {
  private readonly store = inject(Store);

  readonly count = this.store.selectSignal(selectCount);

  increment(): void {
    this.store.dispatch(counterActions.increment());
  }

  decrement(): void {
    this.store.dispatch(counterActions.decrement());
  }

  reset(): void {
    this.store.dispatch(counterActions.reset());
  }
}

selectSignal turns a selector into an Angular signal. When the counter state changes, the signal gets the new value, and every template that reads it updates. providedIn: 'root' gives the whole app one shared instance.

Here is the difference from the component’s point of view:

Without a facade the component knows NgRx

app.ts
export class App {
  private store = inject(Store);

  count = this.store.selectSignal(selectCount);

  increment(): void {
    this.store.dispatch(counterActions.increment());
  }
}

With a facade this project

app.ts
export class App {
  private counterFacade = inject(CounterFacade);

  count = this.counterFacade.count;

  increment(): void {
    this.counterFacade.increment();
  }
}
  • The component stays small and only knows about counting.
  • Tests can swap in a fake facade without setting up a Store.
  • If you move to NgRx SignalStore later, only the facade has to change.

Step 7 · Component

Show the count on screen

The component injects the facade and hands its signal and methods to the template.

src/app/app.ts
import { Component, inject, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';

import { CounterFacade } from './counterStore/counter.facade';

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  templateUrl: './app.html',
  styleUrls: ['./app.scss'],
})
export class App {
  protected readonly title = signal('angular20_Learning');
  private readonly counterFacade = inject(CounterFacade);

  count = this.counterFacade.count;

  increment(): void {
    this.counterFacade.increment();
  }

  decrement(): void {
    this.counterFacade.decrement();
  }

  reset(): void {
    this.counterFacade.reset();
  }
}
src/app/app.html
<h1>Welcome to Angular 20 Learning</h1>

<p>COUNT: &nbsp; {{ count()}}</p>

<div class="counter">
  <button (click)="decrement()">Decrement</button>
  <button (click)="increment()">Increment</button>
  <button (click)="reset()">Reset</button>
</div>

<router-outlet />

count is the same signal the facade exposes, so the template calls it like a function: {{ count() }}. Each button calls a method that forwards to the facade.

Why signals matter here

This app uses provideZonelessChangeDetection(), so there is no zone.js watching every click and timer. Angular re-renders a component when a signal its template reads has changed. A signal from selectSignal fits that model directly. You don’t need the async pipe, a manual subscribe, or ChangeDetectorRef.

app.scss (button styles only)
src/app/app.scss
.counter {
  display: flex;
  width: 300px;
  justify-content: space-between;
  font-size: 1rem;
  font-weight: bold;
  font-family: system-ui, sans-serif;
}

.counter button {
  background-color: #f0f0f0;
  border: none;
  padding: 10px;
  cursor: pointer;
  font-size: 1rem;
  border-radius: 5px;
}

.counter button:active {
  background-color: #e0e0e0;
}

Follow one click through the app

Now put it all together. This is everything that happens when you click Increment while the count is 0.

Template App CounterFacade Store counterReducer selectCount 1 (click) 2 increment() 3 dispatch(action) 4 (state, action) 5 { count: 1 } 6 state changed 7 count signal: 0 → 1 8 count() → re-render
Figure 2. One click, eight steps. Solid arrows are calls going in. Dashed arrows are values coming back out.
  1. 1

    The button’s (click) binding calls increment() on the App component.

  2. 2

    App forwards the call to counterFacade.increment().

  3. 3

    The facade dispatches counterActions.increment(), which is just { type: '[Counter] Increment' }.

  4. 4

    The Store hands the current state, { count: 0 }, and the action to counterReducer.

  5. 5

    The matching on() returns a new object, { count: 1 }. The Store saves it as state.counter.

  6. 6

    The state changed, so the Store lets its selectors know.

  7. 7

    selectCount reads the new slice, and the count signal now holds 1.

  8. 8

    The template reads count(), gets 1, and Angular re-renders that part of the page.

The Store updates the state synchronously during dispatch. Angular then schedules a re-render, because a signal the template reads has changed.

Try it yourself

This panel runs the same reducer logic in plain JavaScript. Click the buttons and watch the path light up in order, the action log grow, and the state change. Click any log entry to see the state right after it, the way Redux DevTools shows it.

The path of one click
(click) facade method action counterReducer new state selectCount count()
Your app
localhost:4200

Welcome to Angular 20 Learning

COUNT:  0

Last action: none yet

Redux DevTools (simulated) 2 / 25 kept
    State after @ngrx/store/update-reducers
    {
      "counter": {
        "count": 0
      }
    }
    Diff
    + counter: { count: 0 }

    The log drops its oldest entry after 25 actions, like maxAge: 25 in app.config.ts. The two @ngrx entries at the top are NgRx’s own startup actions. update-reducers is the moment provideState adds the counter slice, which is why its diff shows counter appearing.

    Debug with Redux DevTools

    1. Install the Redux DevTools extension for Chrome, Edge or Firefox.
    2. Run ng serve and open http://localhost:4200.
    3. Open the browser’s developer tools and choose the Redux tab.

    The left side lists every action in order: NgRx’s startup actions first, then one [Counter] entry per click. Select an action and use the tabs on the right:

    TabWhat it shows
    ActionThe action object, for example { type: '[Counter] Increment' }.
    StateThe whole state tree after that action.
    DiffOnly what changed, for example count: 1 → 2.

    The slider at the bottom moves back and forth through history. This is called time-travel debugging, and it works because every state is a new object that never changes afterwards.

    Two options in app.config.ts control the connection. maxAge: 25 keeps the last 25 actions, so memory doesn’t grow forever. logOnly: !isDevMode() puts the extension in watch-only mode in production builds.

    Mistakes to avoid

    • Don’tChange state in place, like state.count++.
      DoReturn a new object with the spread operator.
    • Don’tPut side effects in a reducer: HTTP calls, localStorage, Date.now(), Math.random().
      DoMove them to an effect. provideEffects() is already registered for this.
    • Don’tInject the Store into every component.
      DoGo through the facade, so components stay simple and easy to test.
    • Don’tType the feature key as a string in two files.
      DoShare one constant, like counterFeatureKey.
    • Don’tSubscribe to store.select() by hand in a zoneless app.
      DoUse selectSignal and read the signal in the template.
    Naming tip

    TypeScript types usually use PascalCase. Renaming countState to CounterState matches Angular’s own style and makes types easy to spot in a file.

    Where to go next

    The counter covers the core loop. These four small changes each add one new idea on top of it.

    Send data with an action

    Use props when an action needs a value. The event name 'Increment By' becomes counterActions.incrementBy.

    counter.actions.ts · counter.reducer.ts · counter.facade.ts
    // counter.actions.ts
    import { createActionGroup, emptyProps, props } from '@ngrx/store';
    
    export const counterActions = createActionGroup({
      source: 'Counter',
      events: {
        Increment: emptyProps(),
        Decrement: emptyProps(),
        Reset: emptyProps(),
        'Increment By': props<{ amount: number }>(),
      },
    });
    
    // counter.reducer.ts
    on(counterActions.incrementBy, (state, { amount }) => ({
      ...state,
      count: state.count + amount,
    })),
    
    // counter.facade.ts
    incrementBy(amount: number): void {
      this.store.dispatch(counterActions.incrementBy({ amount }));
    }

    Let createFeature write the selectors

    createFeature takes a name and a reducer and generates a selector for every property in the state. With it, you could delete counter.selector.ts.

    counter.reducer.ts
    import { createFeature, createReducer, on } from '@ngrx/store';
    
    export const counterFeature = createFeature({
      name: 'counter',
      reducer: createReducer(
        initialState,
        on(counterActions.increment, (state) => ({ ...state, count: state.count + 1 })),
        on(counterActions.decrement, (state) => ({ ...state, count: state.count - 1 })),
        on(counterActions.reset, (state) => ({ ...state, count: 0 })),
      ),
    });
    
    // Generated for you:
    //   counterFeature.selectCounterState
    //   counterFeature.selectCount
    
    // app.config.ts
    provideState(counterFeature),

    Save the count with an effect

    Effects run after the reducer, so the count they read is already the new one. This one writes it to localStorage on every change, which a reducer must never do.

    src/app/counterStore/counter.effects.ts
    import { inject } from '@angular/core';
    import { Actions, createEffect, ofType } from '@ngrx/effects';
    import { Store } from '@ngrx/store';
    import { tap, withLatestFrom } from 'rxjs';
    
    import { counterActions } from './counter.actions';
    import { selectCount } from './counter.selector';
    
    export const saveCount = createEffect(
      (actions$ = inject(Actions), store = inject(Store)) =>
        actions$.pipe(
          ofType(counterActions.increment, counterActions.decrement, counterActions.reset),
          withLatestFrom(store.select(selectCount)),
          tap(([, count]) => localStorage.setItem('count', String(count))),
        ),
      { functional: true, dispatch: false },
    );
    
    // app.config.ts
    // import * as counterEffects from './counterStore/counter.effects';
    // provideEffects(counterEffects),

    Test the reducer

    Reducers are pure functions, so they are the easiest code in the app to test. No TestBed, no Store, no mocks.

    src/app/counterStore/counter.reducer.spec.ts
    import { counterReducer, initialState } from './counter.reducer';
    import { counterActions } from './counter.actions';
    
    describe('counterReducer', () => {
      it('adds 1 on increment', () => {
        const next = counterReducer(initialState, counterActions.increment());
        expect(next.count).toBe(1);
      });
    
      it('leaves the old state untouched', () => {
        counterReducer(initialState, counterActions.increment());
        expect(initialState.count).toBe(0);
      });
    
      it('goes back to 0 on reset', () => {
        const next = counterReducer({ count: 5 }, counterActions.reset());
        expect(next.count).toBe(0);
      });
    });





    How to Remove a File From Your Last Git Commit (Before Pushing)


    It happens to everyone: you run git add . and accidentally commit test data, environment files, or proxy settings you didn't mean to include. 
    If you’ve made a commit with unwanted files but haven't pushed those changes to a remote repository yet, you can easily correct the mistake without losing any of your work locally. 
    This post will walk you through the safest way to remove an unwanted file from your most recent commit, ensuring only the necessary files are pushed.
    The Safest Method: Soft Reset and Selective Recommit 
    The goal here is to undo the commit action while keeping all the file changes in your local working directory. 
    Prerequisites
    • You have not yet pushed the commit to a remote repository (origin, etc.).
    • You want to keep the changes from the unwanted files locally for later use, but exclude them from the current commit. 
    Step-by-Step Instructions
    Follow these commands in your terminal:
    Step 1: Soft reset the last commit. 
    This command moves your branch pointer back by one commit but keeps all the changes from that commit in your staging area. 
    bash
    git reset --soft HEAD~1
    
    Use code with caution.
    Step 2: Unstage the unwanted file(s). 
    The files are currently staged (ready to be committed). Use git reset (in its three-argument form, which unstages files without changing the working directory) to remove specific files from the staging area. 
    bash
    git reset HEAD path/to/unwanted-file.txt
    
    Use code with caution.
    You can repeat this command for multiple files or use wildcards if necessary. The changes for these files are now safely in your working directory, untracked by the next commit. 
    Step 3: Recommit only the correct files. 
    Now, stage only the files you intended to commit originally. 
    bash
    git add path/to/correct-file.txt
    
    Use code with caution.
    Finally, create your new, clean commit. 
    bash
    git commit -m "Your descriptive commit message"
    
    Use code with caution.
    The unwanted files' changes are still present in your working directory, but they are no longer part of your commit history. 

    Therefore, Quick summary:
    bash
    # 1. Undo the last commit, keep changes staged
    git reset --soft HEAD~1
    
    # 2. Unstage the specific files you didn't want in the commit
    git reset HEAD path/to/unwanted-file.txt
    
    # 3. Stage only the correct files
    git add path/to/correct-file.txt
    
    # 4. Create the clean commit
    git commit -m "The correct commit message"

    Thanks for reading, happy coding :)

    Contact Us

    Labels

    aa (6) angular (7) angular testing (1) cc (6) css (1) cypress (1) da (6) daa (6) dm (1) ds (6) flat (4) fom (6) git & github (4) html (33) informative (12) java (5) javascript (3) job updates (13) mse (6) my tools (2) os (6) pccn (7) postman (1) ppl (2) python (27) wt (6)