ngRx Features
Sraban Pahadasingh June 25, 2024 01:24 AMNgRx (pronounced "N-G-R-X") is a powerful library for managing state in Angular applications using reactive programming principles. It is built on top of RxJS, which provides a way to compose asynchronous and event-based programs using observable sequences. NgRx follows the Redux pattern, a predictable state container for JavaScript applications, making it easier to manage and debug the state of your application. Here’s a detailed breakdown of NgRx and its core concepts:
Imagine NgRx as a well-organized factory. Each part of the factory represents a core concept:
- Store: This is the warehouse where all the finished products (state) are stored. It's a single, central location where everything is neatly organized and immutable.
- Actions: These are the orders or requests that come into the factory, specifying what products (state changes) need to be made.
- Reducers: These are the machines in the factory that process raw materials (current state and actions) and produce new products (new state).
- Selectors: These are the quality inspectors that check the products (state) and pick out the parts that are needed.
- Effects: These are the logistic operations that handle external interactions like fetching raw materials (data fetching) and ensure that everything is running smoothly.
Core Concepts of NgRx
-
Store
- The Store is a single source of truth that holds the state of your application. It is an immutable object tree in which the state is represented as a plain JavaScript object. You can think of it as a database that holds all the state of your application.
- Example:
interface AppState { counter: number; }
-
Actions
- Actions are payloads of information that send data from your application to your store. They are the only source of information for the store and follow a strict structure with a
type
field that describes the action being performed and an optionalpayload
. - Example:
export const increment = createAction('[Counter] Increment'); export const decrement = createAction('[Counter] Decrement'); export const reset = createAction('[Counter] Reset');
- Actions are payloads of information that send data from your application to your store. They are the only source of information for the store and follow a strict structure with a
-
Reducers
- Reducers are pure functions that determine how the state changes in response to an action. They take the current state and an action as arguments and return a new state.
- Example:
const initialState: AppState = { counter: 0 }; const counterReducer = createReducer( initialState, on(increment, state => ({ counter: state.counter + 1 })), on(decrement, state => ({ counter: state.counter - 1 })), on(reset, state => ({ counter: 0 })) );
-
Selectors
- Selectors are pure functions used for obtaining slices of the state from the store. They provide a way to access parts of the state and are memoized to improve performance.
- Example:
const selectCounter = (state: AppState) => state.counter;
-
Effects
- Effects handle side effects in NgRx, such as fetching data from a server or logging actions. They listen for dispatched actions, perform operations, and then return new actions to the store.
- Example:
@Injectable() export class CounterEffects { loadCounters$ = createEffect(() => this.actions$.pipe( ofType(loadCounters), mergeMap(() => this.counterService.getAll().pipe( map(counters => loadCountersSuccess({ counters })), catchError(error => of(loadCountersFailure({ error }))) ) ) ) ); constructor( private actions$: Actions, private counterService: CounterService ) {} }
Key Features of NgRx
-
State Management
- NgRx provides a robust state management solution that helps maintain a consistent state across different parts of the application. It encourages the use of a single source of truth and a unidirectional data flow, making state changes predictable and traceable.
-
Immutable State
- The state in NgRx is immutable, meaning it cannot be changed directly. Instead, new states are created based on actions and the current state, which helps prevent accidental mutations and simplifies debugging.
-
Reactive Programming
- NgRx leverages RxJS to handle asynchronous operations and state changes reactively. Observables and operators allow for powerful state management patterns and efficient handling of async data flows.
-
DevTools Integration
- NgRx integrates with Redux DevTools, providing time-travel debugging and state inspection. This makes it easier to understand and debug the state changes in your application.
-
Modularity
- NgRx encourages modular code organization by separating concerns through actions, reducers, selectors, and effects. This separation improves code maintainability and readability.
- Side Effects Management
- NgRx effects handle side effects such as API calls, ensuring that business logic and state management are decoupled and maintainable.
Examples and Use Cases
Basic Counter Example
-
Action Definitions:
export const increment = createAction('[Counter] Increment'); export const decrement = createAction('[Counter] Decrement'); export const reset = createAction('[Counter] Reset');
-
Reducer:
const initialState: AppState = { counter: 0 }; const counterReducer = createReducer( initialState, on(increment, state => ({ counter: state.counter + 1 })), on(decrement, state => ({ counter: state.counter - 1 })), on(reset, state => ({ counter: 0 })) );
-
Selector:
const selectCounter = (state: AppState) => state.counter;
-
Component Integration:
@Component({ selector: 'app-counter', template: ` <div>Current Count: {{ counter$ | async }}</div> <button (click)="increment()">Increment</button> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset</button> ` }) export class CounterComponent { counter$ = this.store.select(selectCounter); constructor(private store: Store<AppState>) {} increment() { this.store.dispatch(increment()); } decrement() { this.store.dispatch(decrement()); } reset() { this.store.dispatch(reset()); } }
NgRx, a popular state management library for Angular applications, has evolved significantly over time. If you're looking for the "old" NgRx structure, you might be interested in the architecture and practices that were common before the significant changes introduced in NgRx version 8 and later.
Below is an overview of the old NgRx structure, covering key concepts such as actions, reducers, effects, selectors, and how they were typically organized and used in Angular applications.
Old NgRx Structure (Pre-NgRx 8)
1. Actions
Actions are payloads of information that are dispatched to the store to signal the type of change or operation to be performed.
Typical Structure:
- Action Creators: Actions were typically created as classes with
type
properties, often defined as string constants.
Example:
// action-types.ts
export const ADD_ITEM = '[Item] Add Item';
export const REMOVE_ITEM = '[Item] Remove Item';
// item.actions.ts
import { Action } from '@ngrx/store';
import { Item } from './item.model';
export class AddItem implements Action {
readonly type = ADD_ITEM;
constructor(public payload: Item) {}
}
export class RemoveItem implements Action {
readonly type = REMOVE_ITEM;
constructor(public payload: number) {} // payload could be an ID
}
export type All = AddItem | RemoveItem;
2. Reducers
Reducers are functions that specify how the state changes in response to an action. They are pure functions that take the current state and an action, returning the new state.
Typical Structure:
- Reducers were often defined in a single file with a switch-case statement to handle different action types.
Example:
// item.reducer.ts
import { ADD_ITEM, REMOVE_ITEM } from './action-types';
import { Item } from './item.model';
import * as ItemActions from './item.actions';
export interface ItemState {
items: Item[];
}
const initialState: ItemState = {
items: []
};
export function itemReducer(state: ItemState = initialState, action: ItemActions.All): ItemState {
switch (action.type) {
case ADD_ITEM:
return {
...state,
items: [...state.items, action.payload]
};
case REMOVE_ITEM:
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
default:
return state;
}
}
3. Selectors
Selectors are used to query slices of the state tree, typically defined in the reducer file.
Typical Structure:
- Selectors were simple functions that extracted parts of the state.
Example:
// item.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ItemState } from './item.reducer';
export const selectItemState = createFeatureSelector<ItemState>('items');
export const selectAllItems = createSelector(
selectItemState,
(state: ItemState) => state.items
);
4. Effects
Effects are used to handle side effects like HTTP requests or other asynchronous operations. They listen to actions dispatched to the store and perform tasks without affecting the state directly.
Typical Structure:
- Effects were typically defined in a separate file and used observables to handle side effects.
Example:
// item.effects.ts
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { Observable } from 'rxjs';
import { map, mergeMap } from 'rxjs/operators';
import { ItemService } from './item.service';
import * as ItemActions from './item.actions';
@Injectable()
export class ItemEffects {
@Effect()
loadItems$: Observable<ItemActions.All> = this.actions$.pipe(
ofType(ItemActions.ADD_ITEM),
mergeMap(action =>
this.itemService.getItems().pipe(
map(items => new ItemActions.AddItem(items))
)
)
);
constructor(private actions$: Actions, private itemService: ItemService) {}
}
5. Store Module Configuration
The StoreModule
and EffectsModule
were typically configured in the AppModule
or a feature module.
Typical Structure:
- Modules were configured to import
StoreModule
andEffectsModule
with reducers and effects.
Example:
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { itemReducer } from './items/item.reducer';
import { ItemEffects } from './items/item.effects';
@NgModule({
declarations: [
// components
],
imports: [
BrowserModule,
StoreModule.forRoot({ items: itemReducer }),
EffectsModule.forRoot([ItemEffects])
],
providers: [],
bootstrap: [/* main component */]
})
export class AppModule { }
6. Feature Modules
For larger applications, NgRx structure typically involved feature modules to encapsulate related actions, reducers, effects, and selectors.
Example:
// items.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { itemReducer } from './item.reducer';
import { ItemEffects } from './item.effects';
@NgModule({
imports: [
CommonModule,
StoreModule.forFeature('items', itemReducer),
EffectsModule.forFeature([ItemEffects])
],
declarations: [
// components
]
})
export class ItemsModule {}
7. Deprecations and Evolutions
Many of these patterns have evolved, with NgRx introducing more concise ways to define actions and reducers. Action creators, createReducer
, and createEffect
functions were introduced to simplify and streamline the code.
Summary
The old NgRx structure focused on a more verbose way of defining actions, reducers, and effects. Over time, the library has introduced new APIs and practices to reduce boilerplate and make the code more maintainable and concise.
For modern practices and more efficient ways to manage state using NgRx, consider looking into the latest NgRx documentation and best practices.