Youcef Madadi
Web and game development teacher
NgRx is a state management library for Angular applications.
Built on RxJS, it uses Reactive Extensions to manage and synchronize application state.
Inspired by Redux but tailored for Angular's ecosystem.
Centralized State Management:
Predictability:
Scalability:
Debugging and DevTools:
Reactive Programming:
Concept | Description |
---|---|
Store | A centralized container for application state. |
State | Immutable data representing the current application condition. |
Actions | Plain objects that describe an intention to change state. |
Reducers | Pure functions that handle state changes in response to actions. |
Effects | Handle side effects like API calls and async operations. |
Selectors | Functions to query specific parts of the state. |
signalStore
The newest approach of Angular NgRx stores
signalStore
?Lightweight and Simple:
Encapsulated State:
Reactive by Design:
Flexible Updates:
update()
without boilerplate.Declarative State Definition:
withState()
to define initial state.State Updates:
update()
function.Reactive Properties:
computed()
.Scoped Usage:
signalStore
signalStore
Works?import { signalStore, withState } from '@ngrx/signals';
const initialState = {
items: [],
isLoading: false,
};
export const ItemsStore = signalStore(
withState(initialState) // Provide the initial state
);
signalStore
Works?import { Component, inject } from '@angular/core';
import { ItemsStore } from './items.store';
@Component({
selector: 'app-items',
template: `
<div *ngIf="store.state().isLoading">Loading...</div>
<ul>
<li *ngFor="let item of store.state().items">{{ item.name }}</li>
</ul>
<button (click)="loadItems()">Load Items</button>
`,
})
export class ItemsComponent {
store = inject(ItemsStore);
loadItems() {
this.store.update((state) => ({
...state,
isLoading: true,
}));
}
}
withComputed
in signalStore
withComputed
?signalStore
that adds computed properties to your store.computed
API, enabling derived state.withComputed
?Simplify Derived State:
Reactive Updates:
Declarative State Logic:
E-commerce Cart:
Filters:
Pagination:
currentPage
or totalPages
.withComputed
Work?type CartState = {
items: { name: string; price: number; quantity: number }[];
};
const initialState: CartState = {
items: [
{ name: 'Book', price: 20, quantity: 2 },
{ name: 'Pen', price: 5, quantity: 4 },
],
};
export const CartStore = signalStore(
withState(initialState),
withComputed(({ items }) => ({
totalItems: () =>items().reduce((sum, item) => sum + item.quantity, 0),
totalPrice: () => items().reduce((sum, item) => sum + item.price * item.quantity, 0),
}))
);
withComputed
Work?import { Component, inject } from '@angular/core';
import { CartStore } from './cart.store';
@Component({
selector: 'app-cart',
template: `
<h1>Cart</h1>
<ul>
<li *ngFor="let item of store.state().items">
{{ item.name }} - {{ item.quantity }} x {{ item.price }}$
</li>
</ul>
<p>Total Items: {{ store.computed().totalItems() }}</p>
<p>Total Price: {{ store.computed().totalPrice() }}$</p>
`,
})
export class CartComponent {
store = inject(CartStore);
}
withMethods
in signalStore
withMethods
?signalStore
to encapsulate business logic and reusable methods in the store.withMethods
?Encapsulation:
Reusability:
Readability:
State Management:
CRUD Operations:
Complex State Transitions:
Reusable Actions:
withMethods
Work?// Add base state and methods
export const CounterStore = signalStore(
withState(initialState),
withMethods((store) => ({
updateTodo(todoId: number, isCompleted: boolean) {
patchState(store, (state) => {
const todoIndex = state.todos.findIndex((todo) => todo.id === todoId);
state.todos[todoIndex].completed = isCompleted;
return { todos: [...state.todos] };
});
}))
);
withHooks
in signalStore
withHooks
?signalStore
that enables lifecycle hooks for the store.withHooks
?Encapsulated Logic:
Resource Management:
Declarative Lifecycle Handling:
Data Fetching:
Subscriptions:
Resource Cleanup:
withHooks
Work?export const UserStore = signalStore(
withState(initialState),
withHooks({
onInit(store) {
console.log('onInit called');
const initialValue = localStorage.getItem(TodoStorageKey);
if (initialValue) {
patchState(store, () => ({
todos: (JSON.parse(initialValue) as TodoI[]) || [],
}));
}
patchState(store, () => ({ isLoading: true }));
timer(2000).subscribe(() => {
patchState(store, () => ({ isLoading: false }));
});
},
})
);
withComputed
Work?export const UserStore = signalStore(
withState(initialState),
withHooks(({ onInit, onDestroy, update }) => {
// onDestroy: Cleanup resources
onDestroy(() => {
console.log('UserStore destroyed');
});
})
);
signalStore
Enhanced User Experience:
Reduce Redundant Fetches:
Enable Offline Functionality:
User Preferences:
Shopping Carts:
Form Drafts:
export const TodoStore = signalStore(
withState(initialState),
withHooks({
onInit: (store) => {
// Load state from localStorage during initialization
const storedTodos = localStorage.getItem('todos');
if (storedTodos) {
store.todos.set({ todos: JSON.parse(storedTodos) });
}
// Persist state to localStorage whenever it changes
effect(() => {
const currentState = getState(store);
localStorage.setItem('todos', JSON.stringify(currentState.todos));
});
},
})
);
Let's practice
By Youcef Madadi