DEV Community

Cover image for NgRx in Angular: A Practical Guide to State Management, Architecture, and Real-World Patterns
Abanoub Kerols
Abanoub Kerols

Posted on

NgRx in Angular: A Practical Guide to State Management, Architecture, and Real-World Patterns

NgRx is more than a store. It is an architecture for managing complex application state in Angular using predictable state transitions, reactive programming, and unidirectional data flow.

Modern Angular applications can start simple, but as the application grows, state management becomes increasingly difficult.

You may eventually have:

  • Components sharing the same data
  • Multiple API requests updating the same state
  • Complex loading and error states
  • Data that must survive navigation
  • Business logic spread across components
  • Race conditions between asynchronous operations
  • Difficult-to-debug state changes

This is where NgRx becomes useful.

In this article, we will build the concepts from the ground up and gradually move toward real-world patterns.


Table of Contents

  1. What Is State Management?
  2. Why Do We Need NgRx?
  3. What Is NgRx?
  4. NgRx Architecture
  5. The Unidirectional Data Flow
  6. Store
  7. Actions
  8. Reducers
  9. Selectors
  10. Effects
  11. Dispatching Actions
  12. Reading State
  13. Complete CRUD Example
  14. Async Operations
  15. Loading and Error States
  16. Entity State
  17. NgRx Entity
  18. Feature-Based State
  19. ComponentStore vs Store
  20. Facades
  21. Immutability
  22. Common NgRx Patterns
  23. Common Mistakes
  24. Performance
  25. Testing
  26. When Should You Use NgRx?
  27. Final Architecture

1. What Is State Management?

Before understanding NgRx, we need to understand state.

State is simply the data that describes the current condition of your application.

For example:

interface User {
  id: number;
  name: string;
  email: string;
}
Enter fullscreen mode Exit fullscreen mode

Your application might have:

interface AppState {
  user: User | null;
  isLoading: boolean;
  error: string | null;
}
Enter fullscreen mode Exit fullscreen mode

At a particular moment:

{
  user: {
    id: 1,
    name: 'John',
    email: 'john@example.com'
  },
  isLoading: false,
  error: null
}
Enter fullscreen mode Exit fullscreen mode

This is application state.


2. Local State vs Global State

Not every piece of state needs NgRx.

Consider a button:

isMenuOpen = false;
Enter fullscreen mode Exit fullscreen mode

This is usually local component state.

You probably don't need a global store for it.

But consider:

Authentication
Products
Shopping Cart
User Profile
Notifications
Permissions
Orders
Enter fullscreen mode Exit fullscreen mode

These may be shared by many parts of the application.

For example:

Navbar
   ↓
User Authentication State
   ↑
Profile Page
   ↑
Checkout
   ↑
Orders
Enter fullscreen mode Exit fullscreen mode

When many components depend on the same state, centralized state management becomes useful.


3. What Is NgRx?

NgRx is a collection of Angular libraries inspired by Redux and built around reactive programming with RxJS.

The core idea is:

Component
   ↓
Action
   ↓
Reducer / Effect
   ↓
Store
   ↓
Selector
   ↓
Component
Enter fullscreen mode Exit fullscreen mode

Instead of allowing components to modify shared state directly, state changes happen through actions.

This creates predictable state transitions.


4. NgRx Architecture

The main pieces are:

                ┌─────────────┐
                │  Component  │
                └──────┬──────┘
                       │
                    dispatch
                       │
                       ▼
                ┌─────────────┐
                │   Action    │
                └──────┬──────┘
                       │
             ┌─────────┴─────────┐
             │                   │
             ▼                   ▼
        ┌──────────┐        ┌──────────┐
        │ Reducer  │        │  Effect  │
        └────┬─────┘        └────┬─────┘
             │                   │
             │                   ▼
             │              HTTP / API
             │                   │
             │                   ▼
             │               Action
             │                   │
             └─────────┬─────────┘
                       ▼
                 ┌──────────┐
                 │  Store   │
                 └────┬─────┘
                      │
                   select
                      │
                      ▼
                ┌─────────────┐
                │  Component  │
                └─────────────┘
Enter fullscreen mode Exit fullscreen mode

There are five concepts you should understand extremely well:

Actions
Reducers
Store
Selectors
Effects
Enter fullscreen mode Exit fullscreen mode

5. The Unidirectional Data Flow

NgRx follows a unidirectional data flow.

That means data moves in a predictable direction.

UI
 ↓
Action
 ↓
State Transition
 ↓
Store
 ↓
Selector
 ↓
UI
Enter fullscreen mode Exit fullscreen mode

For example:

this.store.dispatch(
  increment()
);
Enter fullscreen mode Exit fullscreen mode

The component does not directly change:

count = count + 1;
Enter fullscreen mode Exit fullscreen mode

Instead, it describes what happened:

increment()
Enter fullscreen mode Exit fullscreen mode

The reducer determines how the state should change.


6. The Store

The Store is the centralized state container.

Imagine:

interface AppState {
  counter: number;
  user: User | null;
  products: Product[];
}
Enter fullscreen mode Exit fullscreen mode

Conceptually:

Store
├── counter
├── user
└── products
Enter fullscreen mode Exit fullscreen mode

The Store itself is observable.

You read state reactively:

counter$ = this.store.select(selectCounter);
Enter fullscreen mode Exit fullscreen mode

Then:

<p>{{ counter$ | async }}</p>
Enter fullscreen mode Exit fullscreen mode

7. Actions

An Action describes something that happened.

Example:

import { createAction } from '@ngrx/store';

export const increment = createAction(
  '[Counter] Increment'
);
Enter fullscreen mode Exit fullscreen mode

Another:

export const loadProducts = createAction(
  '[Products Page] Load Products'
);
Enter fullscreen mode Exit fullscreen mode

The string:

[Products Page] Load Products
Enter fullscreen mode Exit fullscreen mode

is the action type.

A good action describes an event.

Prefer:

[Login Page] Login Submitted
Enter fullscreen mode Exit fullscreen mode

over:

[Auth] Set User
Enter fullscreen mode Exit fullscreen mode

Why?

Because the first describes what happened, while the second describes an implementation detail.


8. Actions With Payloads

Actions can carry data.

export const addToCart = createAction(
  '[Cart] Add Product',
  props<{ productId: number }>()
);
Enter fullscreen mode Exit fullscreen mode

Dispatch:

this.store.dispatch(
  addToCart({ productId: 10 })
);
Enter fullscreen mode Exit fullscreen mode

Another example:

export const login = createAction(
  '[Login Page] Login Submitted',
  props<{
    email: string;
    password: string;
  }>()
);
Enter fullscreen mode Exit fullscreen mode

Then:

this.store.dispatch(
  login({
    email: 'john@example.com',
    password: '123456'
  })
);
Enter fullscreen mode Exit fullscreen mode

9. Reducers

Reducers determine how state changes in response to actions.

Example:

import { createReducer, on } from '@ngrx/store';

export const initialState = 0;

export const counterReducer = createReducer(
  initialState,

  on(increment, state => state + 1)
);
Enter fullscreen mode Exit fullscreen mode

The important concept is:

Action
   +
Current State
   ↓
New State
Enter fullscreen mode Exit fullscreen mode

For example:

State = 5

Action = increment

Reducer

5 + 1

New State = 6
Enter fullscreen mode Exit fullscreen mode

10. Reducers Must Be Pure

A reducer should be:

  • Predictable
  • Synchronous
  • Pure
  • Free from side effects

Don't do this inside a reducer:

on(loadProducts, state => {
  http.get('/products');
  return state;
});
Enter fullscreen mode Exit fullscreen mode

Reducers should not perform HTTP requests.

They should only calculate the next state.


11. Immutable State

NgRx relies heavily on immutability.

Bad:

state.user.name = 'John';

return state;
Enter fullscreen mode Exit fullscreen mode

Good:

return {
  ...state,
  user: {
    ...state.user,
    name: 'John'
  }
};
Enter fullscreen mode Exit fullscreen mode

The idea is to create a new state reference instead of mutating the existing one.


12. Selectors

Selectors are used to read data from the Store.

Suppose:

interface CounterState {
  count: number;
}
Enter fullscreen mode Exit fullscreen mode

Create a feature selector:

export const selectCounterState =
  createFeatureSelector<CounterState>('counter');
Enter fullscreen mode Exit fullscreen mode

Then:

export const selectCount = createSelector(
  selectCounterState,
  state => state.count
);
Enter fullscreen mode Exit fullscreen mode

Component:

count$ = this.store.select(selectCount);
Enter fullscreen mode Exit fullscreen mode

Template:

<h1>
  {{ count$ | async }}
</h1>
Enter fullscreen mode Exit fullscreen mode

13. Why Selectors Are Important

You might ask:

Why not simply get the entire state?

Because selectors provide:

Encapsulation

Components don't need to know how state is structured.

Memoization

Selectors can avoid unnecessary recalculations.

Reusability

Multiple components can use the same selector.

Performance

Components subscribe only to the data they need.


14. Derived State

One of the most powerful selector features is derived state.

Suppose:

interface CartState {
  items: CartItem[];
}
Enter fullscreen mode Exit fullscreen mode

You can calculate:

export const selectCartTotal = createSelector(
  selectCartItems,
  items =>
    items.reduce(
      (total, item) => total + item.price * item.quantity,
      0
    )
);
Enter fullscreen mode Exit fullscreen mode

The total does not need to be stored separately.

Instead:

Cart Items
    ↓
Selector
    ↓
Cart Total
Enter fullscreen mode Exit fullscreen mode

This avoids duplicated state.


15. Effects

Effects handle side effects.

Examples:

HTTP Requests
Local Storage
Analytics
Navigation
WebSocket interactions
External APIs
Enter fullscreen mode Exit fullscreen mode

Suppose:

User clicks Load Products
        ↓
loadProducts action
        ↓
Effect
        ↓
HTTP request
        ↓
API response
        ↓
loadProductsSuccess
        ↓
Reducer
        ↓
Store
Enter fullscreen mode Exit fullscreen mode

This is the main purpose of NgRx Effects.


16. Example Effect

@Injectable()
export class ProductsEffects {

  loadProducts$ = createEffect(() =>
    this.actions$.pipe(

      ofType(loadProducts),

      switchMap(() =>
        this.productsService.getProducts().pipe(

          map(products =>
            loadProductsSuccess({ products })
          ),

          catchError(error =>
            of(loadProductsFailure({ error }))
          )

        )
      )

    )
  );

  constructor(
    private actions$: Actions,
    private productsService: ProductsService
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

The effect listens for:

loadProducts
Enter fullscreen mode Exit fullscreen mode

Then performs:

productsService.getProducts()
Enter fullscreen mode Exit fullscreen mode

And dispatches:

loadProductsSuccess
Enter fullscreen mode Exit fullscreen mode

or:

loadProductsFailure
Enter fullscreen mode Exit fullscreen mode

17. The Complete Request Flow

Let's visualize it:

User
 │
 ▼
Products Component
 │
 │ dispatch()
 ▼
loadProducts
 │
 ▼
Products Effect
 │
 │ HTTP
 ▼
Backend API
 │
 ▼
Response
 │
 ▼
loadProductsSuccess
 │
 ▼
Products Reducer
 │
 ▼
Store
 │
 ▼
selectProducts
 │
 ▼
Products Component
Enter fullscreen mode Exit fullscreen mode

This is one of the most important NgRx flows to understand.


18. Loading and Error State

A realistic application needs more than data.

For example:

interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
}
Enter fullscreen mode Exit fullscreen mode

Initial state:

const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null
};
Enter fullscreen mode Exit fullscreen mode

When loading starts:

on(loadProducts, state => ({
  ...state,
  loading: true,
  error: null
}))
Enter fullscreen mode Exit fullscreen mode

Success:

on(loadProductsSuccess, (state, { products }) => ({
  ...state,
  products,
  loading: false
}))
Enter fullscreen mode Exit fullscreen mode

Failure:

on(loadProductsFailure, (state, { error }) => ({
  ...state,
  loading: false,
  error
}))
Enter fullscreen mode Exit fullscreen mode

19. Complete Products Example

Actions

export const loadProducts = createAction(
  '[Products Page] Load Products'
);

export const loadProductsSuccess = createAction(
  '[Products API] Load Products Success',
  props<{ products: Product[] }>()
);

export const loadProductsFailure = createAction(
  '[Products API] Load Products Failure',
  props<{ error: string }>()
);
Enter fullscreen mode Exit fullscreen mode

Reducer

export interface ProductsState {
  products: Product[];
  loading: boolean;
  error: string | null;
}

export const initialState: ProductsState = {
  products: [],
  loading: false,
  error: null
};

export const productsReducer = createReducer(
  initialState,

  on(loadProducts, state => ({
    ...state,
    loading: true,
    error: null
  })),

  on(loadProductsSuccess, (state, { products }) => ({
    ...state,
    products,
    loading: false
  })),

  on(loadProductsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  }))
);
Enter fullscreen mode Exit fullscreen mode

Selectors

export const selectProductsState =
  createFeatureSelector<ProductsState>('products');

export const selectProducts = createSelector(
  selectProductsState,
  state => state.products
);

export const selectLoading = createSelector(
  selectProductsState,
  state => state.loading
);

export const selectError = createSelector(
  selectProductsState,
  state => state.error
);
Enter fullscreen mode Exit fullscreen mode

Component

products$ = this.store.select(selectProducts);
loading$ = this.store.select(selectLoading);
error$ = this.store.select(selectError);

loadProducts() {
  this.store.dispatch(loadProducts());
}
Enter fullscreen mode Exit fullscreen mode

Template

<button (click)="loadProducts()">
  Load Products
</button>

@if (loading$ | async) {
  <p>Loading...</p>
}

@if (error$ | async; as error) {
  <p>{{ error }}</p>
}

@for (product of products$ | async; track product.id) {
  <article>
    <h2>{{ product.name }}</h2>
    <p>{{ product.price }}</p>
  </article>
}
Enter fullscreen mode Exit fullscreen mode

20. NgRx Entity

Imagine you have:

Product[]
Enter fullscreen mode Exit fullscreen mode

with thousands of products.

You frequently need:

Find product by ID
Add product
Remove product
Update product
Select all products
Enter fullscreen mode Exit fullscreen mode

Managing arrays manually becomes repetitive.

NgRx Entity provides utilities for normalized collections.

Conceptually:

Entity State

ids:
[1, 2, 3]

entities:
{
  1: Product,
  2: Product,
  3: Product
}
Enter fullscreen mode Exit fullscreen mode

Instead of:

products.find(p => p.id === id);
Enter fullscreen mode Exit fullscreen mode

the entity adapter can provide selectors and operations for you.


21. Entity Adapter

Example:

const adapter = createEntityAdapter<Product>();
Enter fullscreen mode Exit fullscreen mode

Initial state:

const initialState =
  adapter.getInitialState({
    loading: false,
    error: null
  });
Enter fullscreen mode Exit fullscreen mode

Add products:

adapter.addMany(products, state);
Enter fullscreen mode Exit fullscreen mode

Update:

adapter.updateOne(
  {
    id: product.id,
    changes: product
  },
  state
);
Enter fullscreen mode Exit fullscreen mode

Remove:

adapter.removeOne(productId, state);
Enter fullscreen mode Exit fullscreen mode

This becomes extremely useful for large collections.


22. Feature-Based Architecture

A large Angular application should not have one giant Store file.

Instead:

src/app/
│
├── core/
│
├── shared/
│
└── features/
    │
    ├── auth/
    │   ├── store/
    │   │   ├── auth.actions.ts
    │   │   ├── auth.reducer.ts
    │   │   ├── auth.effects.ts
    │   │   └── auth.selectors.ts
    │   │
    │   └── pages/
    │
    ├── products/
    │   ├── store/
    │   │   ├── products.actions.ts
    │   │   ├── products.reducer.ts
    │   │   ├── products.effects.ts
    │   │   └── products.selectors.ts
    │
    └── cart/
        ├── store/
        │   ├── cart.actions.ts
        │   ├── cart.reducer.ts
        │   ├── cart.effects.ts
        │   └── cart.selectors.ts
Enter fullscreen mode Exit fullscreen mode

This keeps features isolated.


23. NgRx Facade Pattern

Some teams don't want components to know about NgRx directly.

Instead, introduce a facade.

@Injectable({
  providedIn: 'root'
})
export class ProductsFacade {

  products$ = this.store.select(selectProducts);

  loading$ = this.store.select(selectLoading);

  constructor(
    private store: Store
  ) {}

  loadProducts() {
    this.store.dispatch(loadProducts());
  }
}
Enter fullscreen mode Exit fullscreen mode

Component:

constructor(
  private productsFacade: ProductsFacade
) {}

products$ = this.productsFacade.products$;

loadProducts() {
  this.productsFacade.loadProducts();
}
Enter fullscreen mode Exit fullscreen mode

Now the component doesn't know about:

Actions
Selectors
Store
Enter fullscreen mode Exit fullscreen mode

It only knows:

ProductsFacade
Enter fullscreen mode Exit fullscreen mode

This can be useful in large applications.


24. RxJS and NgRx

NgRx relies heavily on RxJS.

For example:

this.store.select(selectProducts)
Enter fullscreen mode Exit fullscreen mode

returns an Observable.

Effects also use RxJS operators:

ofType()
switchMap()
mergeMap()
concatMap()
exhaustMap()
map()
catchError()
Enter fullscreen mode Exit fullscreen mode

Understanding RxJS is therefore extremely valuable when working with NgRx.


25. Choosing the Correct Flattening Operator

This is a common interview topic.

switchMap

Cancels the previous request.

Useful for:

Search
Autocomplete
Enter fullscreen mode Exit fullscreen mode

Example:

actions$.pipe(
  ofType(searchProducts),
  switchMap(...)
);
Enter fullscreen mode Exit fullscreen mode

concatMap

Queues requests.

Request 1
   ↓
Request 2
   ↓
Request 3
Enter fullscreen mode Exit fullscreen mode

Useful when order matters.


mergeMap

Runs requests concurrently.

Request 1 ────────┐
Request 2 ────────┤
Request 3 ────────┘
Enter fullscreen mode Exit fullscreen mode

Useful when requests are independent.


exhaustMap

Ignores new requests while one is running.

Useful for:

Login
Submit button
Payment
Enter fullscreen mode Exit fullscreen mode

For example:

actions$.pipe(
  ofType(login),
  exhaustMap(...)
);
Enter fullscreen mode Exit fullscreen mode

If the user clicks Login multiple times quickly, subsequent clicks are ignored until the first request completes.


26. NgRx vs Services

You don't need NgRx for every Angular application.

A simple service may be enough:

@Injectable({
  providedIn: 'root'
})
export class CartService {

  private cartSubject =
    new BehaviorSubject<CartItem[]>([]);

  cart$ = this.cartSubject.asObservable();

  add(item: CartItem) {
    const current = this.cartSubject.value;

    this.cartSubject.next([
      ...current,
      item
    ]);
  }
}
Enter fullscreen mode Exit fullscreen mode

This can work perfectly well for a small application.

NgRx becomes more valuable when state transitions and interactions become complex.


27. NgRx vs ComponentStore

NgRx Store is designed for centralized application state.

ComponentStore is better suited for localized state.

Example:

Global Application State
        ↓
      Store

Feature-local state
        ↓
  ComponentStore
Enter fullscreen mode Exit fullscreen mode

You don't necessarily need to choose one exclusively.

A large application can use both.


28. What Should NOT Go Into NgRx?

A common mistake is putting everything into the Store.

Don't automatically store:

isDropdownOpen
hoveredItem
temporary input values
modal visibility
simple UI flags
Enter fullscreen mode Exit fullscreen mode

unless there is a real reason.

Ask:

Does this state need to be shared, persisted, coordinated, or observed across multiple parts of the application?

If not, local state may be better.


29. Avoid Duplicated State

Suppose you have:

products: Product[];
selectedProduct: Product;
Enter fullscreen mode Exit fullscreen mode

You may be duplicating information.

Instead, consider:

products: Product[];
selectedProductId: number | null;
Enter fullscreen mode Exit fullscreen mode

Then:

export const selectSelectedProduct = createSelector(
  selectProducts,
  selectSelectedProductId,
  (products, id) =>
    products.find(product => product.id === id) ?? null
);
Enter fullscreen mode Exit fullscreen mode

Now there is one source of truth.


30. Keep Business Logic Out of Components

Bad:

loadProducts() {

  this.loading = true;

  this.http.get('/products')
    .subscribe(products => {

      this.products = products;

      this.loading = false;
    });
}
Enter fullscreen mode Exit fullscreen mode

This mixes:

UI
HTTP
State
Loading
Error handling
Enter fullscreen mode Exit fullscreen mode

With NgRx:

loadProducts() {
  this.store.dispatch(loadProducts());
}
Enter fullscreen mode Exit fullscreen mode

The component becomes much simpler.


31. Smart vs Presentational Components

NgRx works well with the separation between container and presentational components.

Container

Responsible for state interaction.

products$ = this.facade.products$;
Enter fullscreen mode Exit fullscreen mode

Presentational

Receives data:

@Input()
products: Product[] = [];
Enter fullscreen mode Exit fullscreen mode

and emits events:

@Output()
productSelected = new EventEmitter<number>();
Enter fullscreen mode Exit fullscreen mode

Architecture:

Container
   │
   ├── Store
   │
   └── Presentational Component
             │
             └── UI
Enter fullscreen mode Exit fullscreen mode

32. Router Store

NgRx can also integrate router state.

This can allow application state to react to route changes.

Conceptually:

URL
 ↓
Router
 ↓
Router Store
 ↓
Selectors
 ↓
Application
Enter fullscreen mode Exit fullscreen mode

This can be useful when route parameters influence application state.


33. DevTools

One of the major advantages of NgRx is debugging.

With Redux DevTools, you can inspect:

Action
 ↓
Previous State
 ↓
Next State
Enter fullscreen mode Exit fullscreen mode

For example:

[Cart] Add Product

Previous:
items: []

Next:
items: [Product 1]
Enter fullscreen mode Exit fullscreen mode

This makes complex state transitions much easier to understand.


34. Time-Travel Debugging

Because state changes are represented as actions, you can conceptually replay state transitions.

Action 1
Action 2
Action 3
Action 4
Enter fullscreen mode Exit fullscreen mode

Instead of debugging:

"Something changed somewhere."
Enter fullscreen mode Exit fullscreen mode

you can investigate:

"Which action caused the state transition?"
Enter fullscreen mode Exit fullscreen mode

This is one of the architectural strengths of Redux-style state management.


35. Testing Reducers

Reducers are easy to test because they are pure functions.

Example:

it('should increment counter', () => {

  const state = counterReducer(
    5,
    increment()
  );

  expect(state).toBe(6);
});
Enter fullscreen mode Exit fullscreen mode

No HTTP request.

No browser.

No asynchronous operation.

Just:

Input
 ↓
Reducer
 ↓
Output
Enter fullscreen mode Exit fullscreen mode

36. Testing Selectors

Selectors can also be tested independently.

For example:

const state = {
  products: [
    { id: 1, price: 100 },
    { id: 2, price: 200 }
  ]
};
Enter fullscreen mode Exit fullscreen mode

Then verify:

expect(selectProducts.projector(
  state.products
)).toEqual(state.products);
Enter fullscreen mode Exit fullscreen mode

Selectors are another reason to keep business calculations outside components.


37. Testing Effects

Effects require testing asynchronous behavior.

Conceptually:

Input Action
     ↓
Effect
     ↓
Mock API
     ↓
Output Action
Enter fullscreen mode Exit fullscreen mode

You can verify:

loadProducts
       ↓
API called
       ↓
loadProductsSuccess
Enter fullscreen mode Exit fullscreen mode

or:

loadProducts
       ↓
API error
       ↓
loadProductsFailure
Enter fullscreen mode Exit fullscreen mode

38. Common NgRx Mistakes

Mistake 1 — Putting everything in Store

Not every variable belongs in global state.


Mistake 2 — Mutating state

Bad:

state.products.push(product);
return state;
Enter fullscreen mode Exit fullscreen mode

Good:

return {
  ...state,
  products: [
    ...state.products,
    product
  ]
};
Enter fullscreen mode Exit fullscreen mode

Mistake 3 — HTTP inside reducers

Never.

Use Effects or another appropriate side-effect mechanism.


Mistake 4 — Huge selectors

Selectors should remain focused and composable.


Mistake 5 — Components containing business logic

If your component becomes:

500+ lines
Enter fullscreen mode Exit fullscreen mode

and heavily interacts with Store, consider extracting logic.


Mistake 6 — Overusing Effects

Effects are for side effects.

Don't create an effect just to transform simple state synchronously when a reducer or selector is enough.


39. NgRx and Angular Signals

Modern Angular applications increasingly use Signals.

You can bridge Store Observables into Signals.

For example:

products = this.store.selectSignal(selectProducts);
Enter fullscreen mode Exit fullscreen mode

Then in the template:

@for (product of products(); track product.id) {
  <p>{{ product.name }}</p>
}
Enter fullscreen mode Exit fullscreen mode

This gives you a more signal-oriented component API while still using NgRx for centralized state management.


40. A Real-World E-Commerce Architecture

Imagine an e-commerce application.

We could structure the state as:

Application Store
│
├── Auth
│   ├── user
│   ├── token
│   └── loading
│
├── Products
│   ├── entities
│   ├── filters
│   ├── loading
│   └── error
│
├── Cart
│   ├── items
│   └── total
│
├── Orders
│   ├── entities
│   ├── loading
│   └── error
│
└── UI
    ├── notifications
    └── theme
Enter fullscreen mode Exit fullscreen mode

The flow could be:

User
 │
 ▼
Angular Component
 │
 ▼
Action
 │
 ├──────────────┐
 ▼              ▼
Reducer        Effect
 │              │
 ▼              ▼
Store          API
 │              │
 │              ▼
 │         Success Action
 │              │
 └──────────────┘
        │
        ▼
     Selector
        │
        ▼
    Component
Enter fullscreen mode Exit fullscreen mode

This architecture scales significantly better than putting all application logic into components.


41. When Should You Use NgRx?

NgRx is a good choice when your application has:

  • Complex shared state
  • Multiple features depending on the same data
  • Many asynchronous workflows
  • Complex state transitions
  • Large teams
  • Strong debugging requirements
  • Event-driven architecture
  • Need for predictable state changes
  • Significant business logic

Examples:

E-commerce
Banking dashboards
Admin platforms
CRM systems
Large SaaS applications
Enterprise applications
Real-time applications
Enter fullscreen mode Exit fullscreen mode

42. When Should You Avoid NgRx?

For a small application:

5 components
2 API calls
simple forms
little shared state
Enter fullscreen mode Exit fullscreen mode

NgRx may introduce unnecessary complexity.

A simple:

Component
   ↓
Service
   ↓
HTTP
Enter fullscreen mode Exit fullscreen mode

architecture might be better.

The goal is not:

"Use NgRx because NgRx is powerful."

The goal is:

"Use the simplest architecture that can reliably manage the application's complexity."


43. The Most Important Mental Model

If you remember only one thing, remember this:

Action = What happened?

Reducer = How does state change?

Effect = What side effect should happen?

Selector = What state does the UI need?

Store = Where is application state centralized?
Enter fullscreen mode Exit fullscreen mode

For example:

User clicks "Add to Cart"

Action:
[Product Page] Add To Cart

        ↓

Reducer:
Add product to cart state

        ↓

Effect:
Maybe synchronize cart with backend

        ↓

Store:
Updated cart

        ↓

Selector:
Calculate cart count

        ↓

UI:
Cart: 3 items
Enter fullscreen mode Exit fullscreen mode

That is NgRx.


44. Final Architecture

A mature Angular + NgRx application can look like:

                    Angular UI
                        │
                        ▼
                  Component / Facade
                        │
                        ▼
                     Action
                        │
             ┌──────────┴──────────┐
             │                     │
             ▼                     ▼
          Reducer                Effect
             │                     │
             │                  HTTP/API
             │                     │
             │                     ▼
             │               Success/Failure
             │                     │
             └──────────┬──────────┘
                        ▼
                      Store
                        │
                        ▼
                    Selectors
                        │
                        ▼
                  Component / UI
Enter fullscreen mode Exit fullscreen mode

This architecture gives you:

  • Predictable state transitions
  • Centralized state
  • Reactive data flow
  • Testable business logic
  • Clear separation of concerns
  • Better debugging
  • Scalable feature architecture

Conclusion

NgRx is not simply a library for storing variables.

It is a state-management architecture built around a very clear philosophy:

Events
  ↓
Actions
  ↓
State Transitions
  ↓
Store
  ↓
Selectors
  ↓
UI
Enter fullscreen mode Exit fullscreen mode

Effects extend this architecture by handling asynchronous operations and other side effects.

The real power of NgRx appears when an application becomes complex enough that managing state manually starts becoming difficult.

The key is not to memorize APIs.

Understand the architecture first:

Actions
Reducers
Selectors
Effects
Store
Enter fullscreen mode Exit fullscreen mode

Once those concepts are clear, the NgRx APIs become much easier to learn.

And the most important practical rule is:

Don't introduce NgRx because your application is Angular. Introduce it because your application's state management has become complex enough to justify it.

Top comments (0)