NgRx Basics - tutorial01
Okay, let's build a simple Angular standalone application demonstrating core NgRx concepts: Store, Effects, Entity, Component Store, Store DevTools, and the Facade pattern. We'll create a basic "User Management" app where you can load a list of users and add a new user. We'll also add a separate counter component using Component Store. Prerequisites: Node.js and npm/yarn installed. Angular CLI installed (npm install -g @angular/cli). Step 0: Create the Angular Standalone App Open your terminal and run: ng new ngrx-standalone-tutorial --standalone --routing=false --style=css cd ngrx-standalone-tutorial * `--standalone`: Creates the project using the new standalone APIs (no `NgModule`s by default). * `--routing=false`: We don't need routing for this simple example. * `--style=css`: Use plain CSS. Open the project in your favorite code editor. Step 1: Install NgRx Packages Install the necessary NgRx libraries: npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools @ngrx/component-store --save @ngrx/store: Core state management library. @ngrx/effects: For handling side effects like API calls. @ngrx/entity: For managing collections of data (entities). @ngrx/store-devtools: For debugging the store state with browser extensions. @ngrx/component-store: For local component state management. Step 2: Set up NgRx Store and DevTools Configure the global store and DevTools in your application's bootstrap process. Open src/app/app.config.ts. Import the necessary NgRx providers and configure them. // src/app/app.config.ts import { ApplicationConfig, isDevMode } from '@angular/core'; import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideStoreDevtools } from '@ngrx/store-devtools'; export const appConfig: ApplicationConfig = { providers: [ // Provide the global store provideStore(), // We'll add reducers later using provideState // Provide effects provideEffects([]), // We'll add effects later using provideEffects // Provide Store DevTools (only in development) provideStoreDevtools({ maxAge: 25, // Retains last 25 states logOnly: !isDevMode(), // Restrict extension to log-only mode in production autoPause: true, // Pauses recording actions and state changes when the extension window is not open trace: false, // If set to true, will include stack trace for every dispatched action traceLimit: 75, // Maximum stack trace frames to be stored (in case trace is true) connectInZone: true // If set to true, the connection is established within the Angular zone }), ] }; provideStore(): Initializes the global store container. We'll register feature states later. provideEffects([]): Initializes the effects system. We'll register feature effects later. provideStoreDevtools(): Connects your app to the Redux DevTools browser extension. isDevMode() ensures it's only fully active during development. Step 3: Define the State (User Feature) Let's define the structure for managing our users. Create User Model: mkdir src/app/users touch src/app/users/user.model.ts ```typescript // src/app/users/user.model.ts export interface User { id: string; name: string; email: string; } ``` Define User State using NgRx Entity: touch src/app/users/user.state.ts ```typescript // src/app/users/user.state.ts import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; import { User } from './user.model'; // Define the shape of the User feature state // We extend EntityState provided by @ngrx/entity // EntityState gives us { ids: [], entities: {} } structure export interface UserState extends EntityState { // Add other properties for this state slice if needed loading: boolean; error: string | null; selectedUserId: string | null; // Example: if you wanted to track selection } // Create an entity adapter // It provides utility functions to manage the entity collection (add, update, remove, etc.) // Select the primary key (id) // Sort entities by name (optional) export const userAdapter: EntityAdapter = createEntityAdapter({ selectId: (user: User) => user.id, sortComparer: (a: User, b: User): number => a.name.localeCompare(b.name), }); // Define the initial state using the adapter's getInitialState method export const initialUserState: UserState = userAdapter.getInitialState({ // Add initial values for other properties loading: false, error: null, selectedUserId: null, }); ``` * `EntityState`: Provides a standard structure (`ids: string[] | number[]`, `entities: { [id: string | number]: User }`) for storing collections. * `createEntityAdapter`: Creates an adapter with helper functions for common entity operations (add, update, remove, etc.) and selectors. * `userAdapter.getInitialState`: Creates the initial state object, including the `ids` and `entities` properties, p

Okay, let's build a simple Angular standalone application demonstrating core NgRx concepts: Store, Effects, Entity, Component Store, Store DevTools, and the Facade pattern.
We'll create a basic "User Management" app where you can load a list of users and add a new user. We'll also add a separate counter component using Component Store.
Prerequisites:
- Node.js and npm/yarn installed.
- Angular CLI installed (
npm install -g @angular/cli
).
Step 0: Create the Angular Standalone App
-
Open your terminal and run:
ng new ngrx-standalone-tutorial --standalone --routing=false --style=css cd ngrx-standalone-tutorial
* `--standalone`: Creates the project using the new standalone APIs (no `NgModule`s by default).
* `--routing=false`: We don't need routing for this simple example.
* `--style=css`: Use plain CSS.
- Open the project in your favorite code editor.
Step 1: Install NgRx Packages
Install the necessary NgRx libraries:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools @ngrx/component-store --save
-
@ngrx/store
: Core state management library. -
@ngrx/effects
: For handling side effects like API calls. -
@ngrx/entity
: For managing collections of data (entities). -
@ngrx/store-devtools
: For debugging the store state with browser extensions. -
@ngrx/component-store
: For local component state management.
Step 2: Set up NgRx Store and DevTools
Configure the global store and DevTools in your application's bootstrap process.
- Open
src/app/app.config.ts
. - Import the necessary NgRx providers and configure them.
// src/app/app.config.ts
import { ApplicationConfig, isDevMode } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
export const appConfig: ApplicationConfig = {
providers: [
// Provide the global store
provideStore(), // We'll add reducers later using provideState
// Provide effects
provideEffects([]), // We'll add effects later using provideEffects
// Provide Store DevTools (only in development)
provideStoreDevtools({
maxAge: 25, // Retains last 25 states
logOnly: !isDevMode(), // Restrict extension to log-only mode in production
autoPause: true, // Pauses recording actions and state changes when the extension window is not open
trace: false, // If set to true, will include stack trace for every dispatched action
traceLimit: 75, // Maximum stack trace frames to be stored (in case trace is true)
connectInZone: true // If set to true, the connection is established within the Angular zone
}),
]
};
-
provideStore()
: Initializes the global store container. We'll register feature states later. -
provideEffects([])
: Initializes the effects system. We'll register feature effects later. -
provideStoreDevtools()
: Connects your app to the Redux DevTools browser extension.isDevMode()
ensures it's only fully active during development.
Step 3: Define the State (User Feature)
Let's define the structure for managing our users.
-
Create User Model:
mkdir src/app/users touch src/app/users/user.model.ts
```typescript
// src/app/users/user.model.ts
export interface User {
id: string;
name: string;
email: string;
}
```
-
Define User State using NgRx Entity:
touch src/app/users/user.state.ts
```typescript
// src/app/users/user.state.ts
import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity';
import { User } from './user.model';
// Define the shape of the User feature state
// We extend EntityState provided by @ngrx/entity
// EntityState gives us { ids: [], entities: {} } structure
export interface UserState extends EntityState {
// Add other properties for this state slice if needed
loading: boolean;
error: string | null;
selectedUserId: string | null; // Example: if you wanted to track selection
}
// Create an entity adapter
// It provides utility functions to manage the entity collection (add, update, remove, etc.)
// Select the primary key (id)
// Sort entities by name (optional)
export const userAdapter: EntityAdapter = createEntityAdapter({
selectId: (user: User) => user.id,
sortComparer: (a: User, b: User): number => a.name.localeCompare(b.name),
});
// Define the initial state using the adapter's getInitialState method
export const initialUserState: UserState = userAdapter.getInitialState({
// Add initial values for other properties
loading: false,
error: null,
selectedUserId: null,
});
```
* `EntityState`: Provides a standard structure (`ids: string[] | number[]`, `entities: { [id: string | number]: User }`) for storing collections.
* `createEntityAdapter`: Creates an adapter with helper functions for common entity operations (add, update, remove, etc.) and selectors.
* `userAdapter.getInitialState`: Creates the initial state object, including the `ids` and `entities` properties, plus any custom properties we defined (`loading`, `error`).
Step 4: Define Actions
Actions describe unique events that happen in your application (e.g., user clicked "Load Users", data loaded successfully).
touch src/app/users/user.actions.ts
// src/app/users/user.actions.ts
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { User } from './user.model';
// Use createActionGroup for concise action definitions
export const UserActions = createActionGroup({
source: 'Users API', // Unique source for these actions
events: {
// Command Actions (Triggering side effects or direct state changes)
'Load Users': emptyProps(), // Action to initiate loading users
'Add User': props<{ user: Omit<User, 'id'> }>(), // Action to initiate adding a user
// Document Actions (Result of side effects)
'Load Users Success': props<{ users: User[] }>(), // Users loaded successfully
'Load Users Failure': props<{ error: string }>(), // Failed to load users
'Add User Success': props<{ user: User }>(), // User added successfully
'Add User Failure': props<{ error: string }>(), // Failed to add user
},
});
-
createActionGroup
: A helper function to create multiple related actions with a common source. This reduces boilerplate. -
emptyProps()
: For actions that don't carry a payload. -
props<{...}>()
: For actions that carry a payload, defining the payload's type.
Step 5: Create the Reducer
The reducer is a pure function that takes the current state and an action, and returns the new state. It determines how the state changes in response to actions.
touch src/app/users/user.reducer.ts
// src/app/users/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { UserActions } from './user.actions';
import { initialUserState, userAdapter, UserState } from './user.state';
export const userFeatureKey = 'users'; // Unique key for this feature state
export const userReducer = createReducer(
initialUserState,
// Handle Load Users action
on(UserActions.loadUsers, (state): UserState => ({
...state,
loading: true,
error: null,
})),
// Handle Load Users Success action
on(UserActions.loadUsersSuccess, (state, { users }): UserState =>
// Use the adapter's setAll method to replace the existing collection
userAdapter.setAll(users, {
...state,
loading: false,
error: null,
})
),
// Handle Load Users Failure action
on(UserActions.loadUsersFailure, (state, { error }): UserState => ({
...state,
loading: false,
error: error,
})),
// Handle Add User action (Optimistic UI: assume success, handle failure later if needed)
on(UserActions.addUser, (state): UserState => ({
...state,
loading: true, // Indicate loading while adding
error: null,
})),
// Handle Add User Success action
on(UserActions.addUserSuccess, (state, { user }): UserState =>
// Use the adapter's addOne method to add the new user
userAdapter.addOne(user, {
...state,
loading: false, // Stop loading indicator
})
),
// Handle Add User Failure action
on(UserActions.addUserFailure, (state, { error }): UserState => ({
...state,
loading: false, // Stop loading indicator
error: error,
}))
// Add more 'on' handlers for other actions as needed
);
-
createReducer
: Creates the reducer function. -
initialUserState
: The starting state. -
on(Action, (state, payload) => newState)
: Defines how the state should change for a specific action. -
userAdapter.setAll
: Replaces all entities in the state. -
userAdapter.addOne
: Adds a single new entity to the state. The adapter handles updatingids
andentities
immutably. -
userFeatureKey
: A unique string identifier for this slice of state within the global store.
Step 6: Create Selectors
Selectors are pure functions used to get slices of state from the store. They provide memoization, meaning they only recalculate if the relevant part of the state has changed.
touch src/app/users/user.selectors.ts
// src/app/users/user.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { userAdapter, UserState, userFeatureKey } from './user.state';
// 1. Feature Selector: Selects the 'users' feature state slice
export const selectUserState = createFeatureSelector<UserState>(userFeatureKey);
// 2. Entity Adapter Selectors: Use adapter's built-in selectors
const {
selectAll, // Selects the array of users
selectEntities, // Selects the dictionary of users
selectIds, // Selects the array of user ids
selectTotal, // Selects the total count of users
} = userAdapter.getSelectors(selectUserState); // Pass the feature selector here
// 3. Expose the selectors you need
export const selectAllUsers = selectAll;
export const selectUserEntities = selectEntities;
export const selectUserIds = selectIds;
export const selectUserTotal = selectTotal;
// 4. Custom Selectors: Create selectors for specific derived data
export const selectUserLoading = createSelector(
selectUserState,
(state: UserState) => state.loading // Select the loading flag
);
export const selectUserError = createSelector(
selectUserState,
(state: UserState) => state.error // Select the error message
);
// Example: Selector to get a specific user by ID (if needed)
// export const selectUserById = (userId: string) => createSelector(
// selectUserEntities,
// (entities) => entities[userId]
// );
-
createFeatureSelector
: Creates a selector for a top-level feature state (identified byuserFeatureKey
). -
userAdapter.getSelectors
: Provides pre-built, efficient selectors for common entity operations (selectAll, selectTotal, etc.). You must pass your feature selector to it. -
createSelector
: Creates memoized selectors. You can combine other selectors to derive specific data.
Step 7: Create Effects
Effects handle side effects, such as fetching data from an API, based on dispatched actions. They listen for specific actions, perform the side effect, and dispatch new actions (success or failure).
-
Create a Mock User Service:
touch src/app/users/user.service.ts
```typescript
// src/app/users/user.service.ts
import { Injectable } from '@angular/core';
import { Observable, of, delay, throwError, map } from 'rxjs';
import { User } from './user.model';
// Simulate API latency
const API_DELAY = 500;
@Injectable({
providedIn: 'root', // Service available globally
})
export class UserService {
private users: User[] = [
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
];
// Simulate GET /users
getUsers(): Observable {
console.log('UserService: Fetching users...');
return of(this.users).pipe(delay(API_DELAY));
// Simulate error (uncomment to test failure case):
// return throwError(() => new Error('Failed to fetch users')).pipe(delay(API_DELAY));
}
// Simulate POST /users
addUser(userData: Omit): Observable {
console.log('UserService: Adding user...', userData);
// Simulate error (uncomment to test failure case):
// return throwError(() => new Error('Failed to add user')).pipe(delay(API_DELAY));
const newUser: User = {
id: Date.now().toString(), // Generate a simple unique ID
...userData,
};
this.users = [...this.users, newUser]; // Add to our mock DB (immutable update)
return of(newUser).pipe(delay(API_DELAY));
}
}
```
-
Create User Effects:
touch src/app/users/user.effects.ts
```typescript
// src/app/users/user.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, mergeMap, of, switchMap } from 'rxjs';
import { UserService } from './user.service';
import { UserActions } from './user.actions';
@Injectable()
export class UserEffects {
// Effect to handle loading users
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers), // Listen for the 'Load Users' action
mergeMap(() => // Use mergeMap for parallel requests (or switchMap to cancel previous)
this.userService.getUsers().pipe(
map(users => UserActions.loadUsersSuccess({ users })), // On success, dispatch success action
catchError(error => of(UserActions.loadUsersFailure({ error: error.message }))) // On error, dispatch failure action
)
)
)
);
// Effect to handle adding a user
addUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.addUser), // Listen for the 'Add User' action
switchMap(({ user }) => // Use switchMap or mergeMap based on desired behavior
this.userService.addUser(user).pipe(
map(newUser => UserActions.addUserSuccess({ user: newUser })), // On success, dispatch success action
catchError(error => of(UserActions.addUserFailure({ error: error.message }))) // On error, dispatch failure action
)
)
)
);
// Inject Actions stream and UserService
constructor(
private actions$: Actions,
private userService: UserService
) {}
}
```
* `@Injectable()`: Effects are services.
* `createEffect`: Defines an effect. It takes a function that returns an Observable.
* `actions$`: An observable stream of all dispatched actions.
* `ofType(ActionType)`: Filters the actions stream for specific action types.
* `mergeMap`, `switchMap`, `concatMap`, `exhaustMap`: RxJS operators to handle how incoming actions are mapped to service calls (e.g., handle concurrently, cancel previous, queue). `mergeMap` or `switchMap` are common.
* `map`: Transforms the successful result from the service into a success action.
* `catchError`: Catches errors from the service call and transforms them into a failure action using `of()`.
Step 8: Implement the Facade (UserFacade
)
A facade is an optional but often helpful abstraction layer. It simplifies interaction with the store by exposing specific state slices as observables and providing methods to dispatch actions. Components interact with the facade instead of directly with the Store
.
touch src/app/users/user.facade.ts
// src/app/users/user.facade.ts
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { User } from './user.model';
import { UserActions } from './user.actions';
import * as UserSelectors from './user.selectors'; // Import all selectors
@Injectable({ providedIn: 'root' }) // Often provided globally or at feature level
export class UserFacade {
// Expose selectors as observables
readonly allUsers$: Observable<User[]> = this.store.select(UserSelectors.selectAllUsers);
readonly loading$: Observable<boolean> = this.store.select(UserSelectors.selectUserLoading);
readonly error$: Observable<string | null> = this.store.select(UserSelectors.selectUserError);
readonly totalUsers$: Observable<number> = this.store.select(UserSelectors.selectUserTotal);
constructor(private store: Store) {} // Inject the global store
// Expose methods to dispatch actions
loadUsers(): void {
this.store.dispatch(UserActions.loadUsers());
}
addUser(user: Omit<User, 'id'>): void {
this.store.dispatch(UserActions.addUser({ user }));
}
// Add more methods as needed for other actions
}
- Injects
Store
. - Uses
store.select()
with the selectors to expose state streams. - Provides public methods that dispatch actions. The component doesn't need to know about specific actions, only about the facade's methods.
Step 9: Build the Component (user-list.component.ts
)
Now, create the Angular component to display and interact with the user data.
ng generate component users/user-list --standalone
Modify the generated files:
// src/app/users/user-list/user-list.component.ts
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { AsyncPipe, JsonPipe, NgFor, NgIf } from '@angular/common';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; // Import forms modules
import { UserFacade } from '../user.facade'; // Import the Facade
import { provideState } from '@ngrx/store'; // Import provideState
import { provideEffects } from '@ngrx/effects'; // Import provideEffects
import { userFeatureKey, userReducer } from '../user.reducer'; // Import feature reducer/key
import { UserEffects } from '../user.effects'; // Import feature effects
@Component({
selector: 'app-user-list',
standalone: true,
imports: [
NgIf, // Add NgIf and NgFor
NgFor,
AsyncPipe, // Add AsyncPipe for observables
JsonPipe, // Add JsonPipe for debugging (optional)
ReactiveFormsModule // Add ReactiveFormsModule
],
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush, // Use OnPush for performance with NgRx
providers: [
// Register the feature state and effects specifically for this component's scope
// or provide them globally/at a route level if shared across many components.
// For this simple example, we provide them here.
provideState(userFeatureKey, userReducer),
provideEffects([UserEffects]),
// UserFacade is already providedIn: 'root'
]
})
export class UserListComponent implements OnInit {
// Inject the Facade
constructor(public userFacade: UserFacade, private fb: FormBuilder) {}
// Form to add a new user
addUserForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
ngOnInit(): void {
// Dispatch action via facade on component initialization
this.userFacade.loadUsers();
}
onSubmit(): void {
if (this.addUserForm.valid) {
const { name, email } = this.addUserForm.value;
if (name && email) { // Type assertion needed due to partial value possibility
// Dispatch action via facade
this.userFacade.addUser({ name, email });
this.addUserForm.reset(); // Clear the form
}
}
}
}
User Management (NgRx Global Store + Entity + Effects + Facade)
*ngIf="userFacade.loading$ | async">Loading users...
*ngIf="userFacade.error$ | async as error" style="color: red;">
Error: {{ error }}
*ngIf="(userFacade.allUsers$ | async)?.length === 0 && !(userFacade.loading$ | async)">
No users found.
*ngIf="(userFacade.allUsers$ | async)?.length > 0">
- *ngFor="let user of (userFacade.allUsers$ | async)">
{{ user.name }} ({{ user.email }})
Total Users: {{ userFacade.totalUsers$ | async }}
Add New User
-
Standalone Component:
standalone: true
andimports
array for dependencies. -
provideState
&provideEffects
: These functions register the feature's reducer and effects. Placing them in the component'sproviders
makes them available when this component (or its children) is loaded. For larger apps, you might provide these at a route level for lazy loading. -
ChangeDetectionStrategy.OnPush
: Improves performance. Angular will only check this component for changes if its inputs change, an event originates from it, or an observable it subscribes to (viaAsyncPipe
) emits a new value. -
Inject
UserFacade
: The component interacts solely with the facade. -
AsyncPipe
: Subscribes to the observables from the facade automatically, handles unsubscription, and triggers change detection when new values arrive. -
ReactiveFormsModule
: Used for the "Add User" form. -
Dispatching Actions: Calls facade methods (
loadUsers()
,addUser()
) to trigger state changes.
Step 10: Integrate into app.component.ts
Import and use the UserListComponent
in your main AppComponent
.
// src/app/app.component.ts
import { Component } from '@angular/core';
import { UserListComponent } from './users/user-list/user-list.component'; // Import the component
// Import CounterComponent later when created
@Component({
selector: 'app-root',
standalone: true,
imports: [
UserListComponent, // Add UserListComponent here
// CounterComponent will be added here later
],
template: `
NgRx Standalone Tutorial
`,
styleUrls: ['./app.component.css'],
})
export class AppComponent {
title = 'ngrx-standalone-tutorial';
}
Step 11: Add NgRx Component Store Example
Component Store is designed for local component state management. It's simpler and self-contained, useful for state that doesn't need to be shared globally.
-
Create Counter Store Service:
mkdir src/app/counter touch src/app/counter/counter.store.ts
```typescript
// src/app/counter/counter.store.ts
import { Injectable } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { Observable, tap } from 'rxjs';
// 1. Define the state interface for this store
export interface CounterState {
count: number;
updates: number;
}
// 2. Provide the initial state
const initialState: CounterState = {
count: 0,
updates: 0,
};
@Injectable() // Provide locally in the component that uses it
export class CounterStore extends ComponentStore {
// 3. Initialize the store with the initial state
constructor() {
super(initialState);
console.log('CounterStore initialized');
}
// 4. Define Selectors (directly within the store)
readonly count$: Observable = this.select(state => state.count);
readonly updates$: Observable = this.select(state => state.updates);
readonly viewModel$ = this.select({ // Combine selectors for a view model
count: this.count$,
updates: this.updates$
});
// 5. Define Updaters (synchronous state modifications)
// An updater takes the current state and returns the partial state to update.
readonly increment = this.updater((state) => ({
count: state.count + 1,
updates: state.updates + 1,
}));
readonly decrement = this.updater((state) => ({
count: state.count - 1,
updates: state.updates + 1,
}));
// An updater can also take parameters
readonly setCount = this.updater((state, value: number) => ({
count: value,
updates: state.updates + 1
}));
// 6. Define Effects (asynchronous operations)
// Example: An effect that updates the count after a delay
readonly delayedIncrement = this.effect((trigger$: Observable) => {
return trigger$.pipe(
tap(() => console.log('Starting delayed increment...')),
// Use switchMap, mergeMap etc. as needed
// Here we just simulate an action
tap(() => {
// Effects can call updaters or patchState directly
this.patchState(state => ({ count: state.count + 5, updates: state.updates + 1 }));
console.log('Delayed increment finished.');
})
);
});
// Optional: Log state changes (useful for debugging)
// readonly logState = this.effect(() => {
// return this.state$.pipe(tap(state => console.log('Counter State:', state)));
// });
}
```
* `ComponentStore`: Extends the base class, typed with the state shape.
* `constructor()`: Calls `super(initialState)` to set the initial state.
* `select()`: Creates observables for state slices.
* `updater()`: Creates functions for *synchronous* state updates. They are pure functions modifying state immutably under the hood.
* `effect()`: Creates functions for handling *asynchronous* side effects specific to this component's state. Effects typically call updaters or `patchState` upon completion.
* `patchState()`: Another way to update state, accepting a partial state object or a function.
-
Create Counter Component:
ng generate component counter/counter --standalone
```typescript
// src/app/counter/counter/counter.component.ts
import { Component, ChangeDetectionStrategy, OnDestroy } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { CounterStore } from '../counter.store'; // Import the store
@Component({
selector: 'app-counter',
standalone: true,
imports: [AsyncPipe, NgIf],
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
// Provide the CounterStore locally for this component and its children
providers: [CounterStore],
})
export class CounterComponent implements OnDestroy {
// Inject the LOCAL CounterStore
constructor(public counterStore: CounterStore) {}
increment(): void {
this.counterStore.increment(); // Call updater
}
decrement(): void {
this.counterStore.decrement(); // Call updater
}
reset(): void {
this.counterStore.setCount(0); // Call updater with value
}
delayedIncrement(): void {
this.counterStore.delayedIncrement(); // Trigger effect
}
ngOnDestroy(): void {
console.log('CounterComponent destroyed, CounterStore will be cleaned up.');
// ComponentStore automatically cleans up subscriptions when the component is destroyed
}
}
```
```html
Local Counter (NgRx Component Store)
Current Count: {{ vm.count }}
Total Updates: {{ vm.updates }}
```
* `providers: [CounterStore]`: This is crucial. It provides a *new instance* of `CounterStore` specifically for this component instance and its descendants. The state is local and ephemeral.
* The component directly calls methods (`increment`, `decrement`, `delayedIncrement`) on the injected `counterStore` instance.
* It uses `AsyncPipe` to subscribe to observables exposed by the `counterStore`.
-
Add Counter Component to
app.component.ts
:
// src/app/app.component.ts import { Component } from '@angular/core'; import { UserListComponent } from './users/user-list/user-list.component'; import { CounterComponent } from './counter/counter/counter.component'; // Import CounterComponent @Component({ selector: 'app-root', standalone: true, imports: [ UserListComponent, CounterComponent, // Add CounterComponent here ], template: `
NgRx Standalone Tutorial
`, styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'ngrx-standalone-tutorial'; }
Step 12: Run and Verify
-
Run the App:
npm start
Or
ng serve -o
. -
Open Redux DevTools: Install the Redux DevTools browser extension if you haven't already. Open your browser's developer console, go to the Redux tab.
- You should see actions like
[Users API] Load Users
,[Users API] Load Users Success
being dispatched. - Inspect the state changes and the overall state structure (
users
slice). - Try adding a user and observe the
[Users API] Add User
and[Users API] Add User Success
actions and state updates. -
Important: Notice that the Counter state managed by
ComponentStore
does not appear in the Redux DevTools, as it's local component state, not part of the global NgRx Store.
- You should see actions like
Interact with the Counter: Click the buttons on the counter component and see its local state update independently.
Explanation of Concepts:
-
NgRx Store (
@ngrx/store
):- What: The core library. Provides a centralized, immutable state container (the Store) for your entire application (or large feature areas).
- Why: Predictable state management, single source of truth, easier debugging, enables powerful tooling (like DevTools). Good for state shared across many components or features.
-
How:
provideStore()
(global setup),provideState()
(feature setup),Store
service (injection), Actions (events), Reducers (state changes), Selectors (state retrieval). - Reference: https://ngrx.io/guide/store
-
Actions (
@ngrx/store
):-
What: Plain objects describing events that have occurred (e.g., user login requested, data loaded). They have a
type
and an optionalpayload
.createActionGroup
simplifies creation. - Why: The only way to trigger state changes in the global store. Decouples "what happened" from "how the state changes". Enables tracing state updates.
-
How: Define using
createActionGroup
orcreateAction
. Dispatch usingstore.dispatch(action)
. - Reference: https://ngrx.io/guide/store/actions
-
What: Plain objects describing events that have occurred (e.g., user login requested, data loaded). They have a
-
Reducers (
@ngrx/store
):- What: Pure functions responsible for handling actions and calculating the next state based on the current state and the action. Must be immutable (return a new state object, don't modify the existing one).
- Why: Ensure predictable state transitions. Keep state logic separate from components.
-
How: Use
createReducer
and theon
function to handle specific actions. Often used with NgRx Entity adapters for collection management. Registered usingprovideState(featureKey, reducer)
. - Reference: https://ngrx.io/guide/store/reducers
-
Selectors (
@ngrx/store
):- What: Pure functions to query and derive data from the store state. They are memoized for performance.
- Why: Provide optimized access to state. Decouple components from the state structure. Allow composing complex queries from simple ones.
-
How: Use
createFeatureSelector
(for feature slice) andcreateSelector
(for specific data points). NgRx Entity adapters providegetSelectors
. Use withstore.select()
or theasync
pipe. - Reference: https://ngrx.io/guide/store/selectors
-
NgRx Effects (
@ngrx/effects
):- What: A way to handle side effects (API calls, WebSockets, browser storage interactions) triggered by actions. Effects listen for actions, perform tasks, and dispatch new actions (usually success/failure).
- Why: Isolate side effects from components and reducers (reducers must be pure). Centralize interaction logic with external resources.
-
How: Create injectable service classes. Use
createEffect
,Actions
stream,ofType
operator, RxJS operators (map
,mergeMap
,catchError
), and dispatch resulting actions. Registered usingprovideEffects([MyEffects])
. - Reference: https://ngrx.io/guide/effects
-
NgRx Entity (
@ngrx/entity
):-
What: An adapter library for managing collections of entities (like users, products, posts) in the store. Provides a standard state shape (
ids
,entities
) and utility functions/selectors. - Why: Reduces boilerplate for common CRUD operations on collections. Provides optimized selectors for entity data. Enforces a consistent structure.
-
How:
createEntityAdapter
,adapter.getInitialState
, adapter methods (addOne
,setAll
,updateOne
,removeOne
, etc.) used within reducers,adapter.getSelectors
used to create efficient selectors. - Reference: https://ngrx.io/guide/entity
-
What: An adapter library for managing collections of entities (like users, products, posts) in the store. Provides a standard state shape (
-
Facade Pattern (Not an NgRx library, but a pattern used with NgRx):
- What: An injectable service that acts as an intermediary between components and the NgRx Store/Effects. It exposes simplified observables (using selectors) and methods (which dispatch actions).
- Why: Hides NgRx complexity (actions, selectors, dispatching) from components. Provides a clear API for interacting with a specific feature's state. Makes components leaner and easier to test. Improves maintainability.
-
How: Create an injectable service. Inject
Store
. Usestore.select(selector)
to expose state observables. Create methods that callstore.dispatch(action)
. Inject the facade into components.
-
NgRx Component Store (
@ngrx/component-store
):- What: A standalone library for managing local component state. It's simpler, self-contained within a component (or a directive/service provided locally), and doesn't interact with the global NgRx Store or DevTools by default.
- Why: Perfect for state that isn't shared globally (UI state, form state, temporary data). Less boilerplate than setting up full NgRx Store features. Improves performance by keeping state local. Automatic cleanup when the component is destroyed.
-
How: Create an injectable service extending
ComponentStore
. Define selectors (select
), updaters (updater
), and effects (effect
) within the store service. Provide it locally in the component'sproviders
array. Inject and use directly in the component. - Reference: https://ngrx.io/guide/component-store
-
NgRx Store DevTools (
@ngrx/store-devtools
):- What: Connects your global NgRx Store to the Redux DevTools browser extension.
- Why: Essential for debugging. Allows you to inspect the sequence of actions, view state snapshots at different points in time ("time travel debugging"), and see how state changed.
-
How: Install package.
provideStoreDevtools({...})
inapp.config.ts
, usually configured only for development mode (isDevMode()
). - Reference: https://ngrx.io/guide/store-devtools
This comprehensive example covers the setup and basic usage of the core NgRx libraries within a modern Angular standalone application structure. Remember to adapt the patterns (especially Facades and where state lives - global vs. local) based on the specific needs and complexity of your real-world projects.