Short Description
Explore how NgRx helps you manage complex application state in Angular using the power of Redux-inspired architecture. This talk demystifies NgRx concepts like Store, Actions, Reducers, Effects, and Selectors — with real-world examples to help you write scalable and maintainable code.
Short Summary
As Angular applications grow, managing shared state becomes increasingly challenging. In this talk, we’ll dive into NgRx, a reactive state management library for Angular based on the Redux pattern. You’ll learn:
- What state management is and why it’s critical in modern apps
- Core NgRx concepts: Store, Actions, Reducers, Selectors, and Effects
- How to set up and structure NgRx in a scalable Angular project
- Best practices and common pitfalls
- Real-world examples of using NgRx for managing UI state, API calls, and more
By the end of the session, you’ll have a clear understanding of how to use NgRx effectively to build robust, maintainable, and scalable Angular applications.
Intro:
As Angular applications scale in complexity, managing shared state across components becomes a significant hurdle. NgRx, a powerful state management library for Angular inspired by the Redux pattern, offers a robust solution. With the recent introduction of Angular Signals, NgRx has evolved, providing a more streamlined and modern approach to state management that is both highly performant and developer-friendly. This new paradigm, centered around the @ngrx/signals package and its SignalStore, simplifies the architecture while retaining the core principles of predictable state management.
What state management is and why it’s critical in modern apps
In modern web applications, multiple components often need to access and manipulate the same data. Without a centralized strategy, this can lead to a tangled web of data flows, making the application difficult to debug, maintain, and scale. State management libraries like NgRx provide a single source of truth for your application’s state, ensuring that data flows in a predictable, unidirectional manner. This predictability is crucial for building robust and maintainable applications.
Core Concepts of Modern NgRx with Signals
The classic NgRx architecture, with its explicit Actions, Reducers, Effects, and Selectors, has been simplified with the introduction of @ngrx/signals. While the foundational principles remain, their implementation is now more aligned with Angular’s reactive signal-based system.
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. |
Structuring a Scalable NgRx Project with Signals
A key advantage of the SignalStore is the ability to co-locate related state, updaters, and effects, often within a single feature-specific store file. This promotes better organization and modularity.
A typical structure might involve:
- Feature-Based Stores: Instead of a single global store, applications are often composed of multiple, smaller SignalStore instances, each managing the state for a specific feature (e.g., products.store.ts, cart.store.ts).
- Providing the Store: A SignalStore can be provided at the root level of the application or at the component level, depending on its scope of use.
- Component Interaction: Components inject the relevant store and interact with it by calling its methods to trigger state changes and subscribing to its state signals to react to updates.
A Glimpse into the Code: Real-World Example
Let’s consider a simple counter feature to illustrate the concepts:
set up:
Package Installation
First, you’ll need to add the core NgRx packages to your Angular project. Open your terminal in the project’s root directory and run the following command:
ng add @ngrx/store
This command installs the main NgRx store package. Next, install the signals package:
npm install @ngrx/signals
These two packages, @ngrx/store and @ngrx/signals, are all you need to get started with the modern, signal-based approach.
Setting Up Your First SignalStore
Setting up a SignalStore is straightforward. You define the store’s shape, state, and methods in a single file.
Step 1: Create the Store File
Create a new file for your store, for example, counter.store.ts. This file will contain all the logic for managing the counter’s state.
Step 2: Define the Store
Inside counter.store.ts, use the signalStore function to define the store’s structure. You’ll use helper functions like withState and withMethods to build it.
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);
},
}))
);
Key parts of this setup:
- signalStore: The main function to create your store.
- { providedIn: 'root’ }: This makes your CounterStore a singleton service that can be injected anywhere in your application.
- withState(initialState): Defines the initial state of your store.
- withMethods(…): Defines methods that can be called to interact with the store.
- patchState(…): The function used inside methods to safely and immutably update the state.
Step 3: Use the Store in a Component
Now you can inject and use your CounterStore in 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 now reads state directly from the store.count() signal and calls methods like store.increment() to update it. The view will automatically react to any state changes. Basically in this example, the CounterStore defines the initial state and the methods to manipulate it. The CounterComponent injects the store and directly calls these methods in response to user interactions. The template then reactively displays the count signal from the store.
Best Practices and Common Pitfalls
- Embrace Immutability: Always treat state as immutable. The patchState function helps enforce this by creating a new state object with the updated values.
- Keep Stores Focused: Avoid creating a single monolithic store. Instead, break down your application state into logical, feature-based stores.
- Leverage withComputed: For derived data, use withComputed to create memoized selectors. This prevents redundant calculations and improves performance.
- Handle Side Effects in Methods: Co-locate asynchronous operations within the store’s methods using utilities like rxMethod. This makes the data flow easier to follow.
- Avoid Over-fetching: Design your state and selectors to fetch only the data needed by the components, preventing unnecessary re-renders.
By embracing NgRx with signals, you can build Angular applications that are not only scalable and performant but also more intuitive and maintainable, effectively taming the complexities of modern application state.
Note: To properly master and implement the state management approach we’ve been discussing, you should be working with Angular v17 or a more recent stable version.
To learn more about signal store and ngrx you can check out these articles too! ->