Overview
Dive into NgRx and learn how this Redux-inspired library brings order to complex Angular state. This session breaks down the essentials — Store, Actions, Reducers, Effects, and Selectors — using practical examples that show you how to write code that scales without losing clarity.
Talk Highlights
Handling shared state gets messy as Angular apps grow. This presentation focuses on NgRx, a reactive state management tool for Angular built on the Redux pattern. You’ll walk away knowing:
- Why state management matters and where it fits in today’s apps
- The nuts and bolts of NgRx: Store, Actions, Reducers, Selectors, and Effects
- How to organize NgRx in a project that’s ready to grow
- Which habits to adopt and which errors to avoid
- Concrete use cases for NgRx in UI state, server calls, and beyond
By the end, you’ll have a solid grasp of NgRx and how to leverage it for Angular apps that are dependable, clean, and ready for scale.
Introduction:
When Angular apps get bigger, sharing state between components becomes a real pain point. NgRx — a state management library for Angular inspired by the Redux pattern — offers a reliable answer. With the arrival of Angular Signals, NgRx has adapted, offering a more current and streamlined take on state handling that’s both efficient and easy to work with. This refreshed approach, built around the @ngrx/signals package and its SignalStore, cuts through the complexity while sticking to the principles of predictable state flow.
Understanding state management and its importance in today’s apps
In contemporary web applications, many components frequently need to read and modify the same data. Without a centralized approach, data flow can become chaotic, which makes debugging, upkeep, and scaling a challenge. State management libraries like NgRx establish a single source of truth for your app’s state, guaranteeing that data moves in a steady, one-way path. This consistency is key to creating apps that are solid and simple to maintain.
Core Concepts of Modern NgRx with Signals
The original NgRx setup, with its explicit Actions, Reducers, Effects, and Selectors, has been streamlined with the advent of @ngrx/signals. Although the underlying ideas are unchanged, their application now fits more naturally with Angular’s reactive signal-based model.
| Traditional NgRx | NgRx with Signals (SignalStore) | Description |
| Store | SignalStore | The single, centralized source of truth for the application’s state. It is now created using the signalStore function. |
| Actions | Methods within withMethods | In the new model, explicit action dispatches are often replaced by calling methods directly on the store. These methods describe unique events that can lead to state changes or side effects. |
| Reducers | withState & patchState | The initial state is defined using the withState utility. State changes are handled by methods that use patchState to immutably update the state. |
| Selectors | withComputed | These are functions that derive and memoize data from the state. withComputed allows for the creation of derived signals that automatically update when the state they depend on changes. |
| Effects | rxMethod & Methods in withMethods | Side effects, such as asynchronous API calls, are now primarily managed within the store’s methods, often using the rxMethod utility for RxJS integration. This co-locates the action and its corresponding side effect. |
Organizing a Growable NgRx Project with Signals
One of the big wins with the SignalStore is how it lets you group related state, updates, and effects together, often in one file tailored to a specific feature. This setup encourages better structure and modularity.
A common arrangement looks like this:
- Feature-Based Stores: Rather than one big global store, apps are typically made up of several smaller SignalStore instances, each one handling the state for its own feature (for example, products.store.ts or cart.store.ts).
- Providing the Store: A SignalStore can be supplied at the app root or at the component level, depending on where it’s needed.
- Component Interaction: Components pull in the relevant store and engage with it by invoking its methods to trigger changes and by subscribing to its state signals to stay in sync with updates.
A Look at the Code: Practical Example
We’ll use a basic counter feature to demonstrate the core ideas:
get started:
Installing the Packages
To begin, you’ll need to bring the core NgRx packages into your Angular project. Open your terminal in the project’s main folder and run this command:
ng add @ngrx/store
That command brings in the essential NgRx store package. Following that, install the signals package:
npm install @ngrx/signals
With just these two packages, @ngrx/store and @ngrx/signals, you’re ready to work with the new signal-based approach.
Building Your First SignalStore
Creating a SignalStore is simple. You lay out the store’s structure, state, and functions all in one file.
Step 1: Make the Store File
Start by creating a new file for your store, like counter.store.ts. This file will hold all the logic for the counter’s state.
Step 2: Define the Store
In counter.store.ts, you’ll use the signalStore function to outline the store’s structure. You’ll rely on supporting functions like withState and withMethods to put it together.
counter.store.ts
import { signalStore, withState, withMethods, patchState } from '@ngrx/signals';
// Define the shape of the state
export interface CounterState {
count: number;
}
// Set the initial state
const initialState: CounterState = {
count: 0,
};
// Create the SignalStore
export const CounterStore = signalStore(
{ providedIn: 'root' }, // Makes the store available app-wide
withState(initialState), // Adds the state to the store
// Adds methods to update the state
withMethods((store) => ({
increment() {
// Use patchState to immutably update the state
patchState(store, { count: store.count() + 1 });
},
decrement() {
patchState(store, { count: store.count() - 1 });
},
reset() {
patchState(store, initialState);
},
}))
);
Here’s what each piece does:
- signalStore: The core function for creating your store.
- { providedIn: 'root’ }: This sets up your CounterStore as a singleton service, so it can be injected from anywhere in your app.
- withState(initialState): Outlines the starting state of your store.
- withMethods(…): Defines the methods you can call to work with the store.
- patchState(…): The tool used inside methods to update state safely and immutably.
Step 3: Use the Store in a Component
Now you can bring your CounterStore into any component.
counter.component.ts
import { Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';
@Component({
selector: 'app-counter',
standalone: true, // Make sure it's a standalone component
template: `
<h2>Counter: {{ store.count() }}</h2>
<button (click)="store.increment()">Increment</button>
<button (click)="store.decrement()">Decrement</button>
<button (click)="store.reset()">Reset</button>
`,
})
export class CounterComponent {
// Inject the store directly into the component
readonly store = inject(CounterStore);
}
Your component reads state straight from the store.count() signal and calls methods like store.increment() to alter it. The view updates automatically when any state changes. In short, the CounterStore sets the initial state and offers methods to modify it. The CounterComponent injects the store and directly calls these methods when users interact. The template then reactively shows the count signal from the store.

Recommended Practices and Pitfalls to Avoid
- Stick to Immutability: Always handle state as immutable. The patchState function helps with this by generating a fresh state object that includes the updated values.
- Keep Stores Focused: Don’t create one giant store. Split your application state into logical, feature-specific stores instead.
- Make Use of withComputed: For derived data, turn to withComputed to build memoized selectors. This cuts down on unnecessary calculations and boosts performance.
- Manage Side Effects in Methods: Co-locate async tasks within the store’s methods using helpers like rxMethod. This keeps the data flow clear and easy to trace.
- Steer Clear of Over-fetching: Shape your state and selectors to grab only what components need, which prevents needless re-renders.
By adopting NgRx with signals, you can craft Angular applications that are not just scalable and efficient but also more straightforward and easier to maintain, successfully bringing order to the chaos of modern application state.
Heads up: To fully make use of the state management approach covered here, you’ll want to be on Angular v17 or a newer stable release.
For a deeper dive into signal store and ngrx, check out these additional reads ->
