Showing posts with label NGRX. Show all posts
Showing posts with label NGRX. Show all posts

Tuesday, 29 April 2025

angular NGRX with adapter

 EntityAdapter

The Entity Adapter in the example serves as a boilerplate-killer and state normalizer for your collection of Item entities. Its main purposes are:

  1. Normalize your state shape
    Instead of hand-rolling

    ts

    { ids: string[]; entities: { [id: string]: Item }; loading: boolean; error: string | null; }

    the adapter gives you that structure automatically (with its ids array and entities lookup map), plus whatever extra flags you pass in (loading, error).

  2. Immutable update helpers
    addOne, addMany, updateOne, removeOne, removeAll, etc.
    Each method takes your current state and returns a brand-new state with exactly the right pieces changed (and never mutates the old state). In the example:

    • On createItemSuccess, itemAdapter.addOne(item, state) inserts the new item into both ids and entities in one call.

    • On clearItems, itemAdapter.removeAll(state) wipes out every item but lets you preserve or explicitly reset your extra flags (loading, error).

  3. Selectors out of the box
    You don’t have to write boilerplate selectors to read allItems, entitiesById, or totalCount. A single call to itemAdapter.getSelectors() gives you typed, memoized selectors like selectAll and selectEntities.

  4. Consistency & performance
    By centralizing all add/update/remove logic in one tested library, you avoid subtle bugs (forgot to update one part of the state) and get optimized updates (e.g. quick lookups via the map rather than array scans).


Effects code explanation
createItem$ = createEffect(() =>
  this.actions$.pipe(
    ofType(ItemsActions.createItem),
    mergeMap(({ item }) =>
      this.itemService.create(item).pipe(
        map(created => ItemsActions.createItemSuccess({ item: created })),
        catchError(err =>
          of(ItemsActions.createItemFailure({ error: err.message }))
        )
      )
    )
  )
);
  • Declares an Effect

    • createEffect(() => …) tells NgRx “here is a stream of work I want you to run whenever actions flow through.”

    • The returned observable (the inner this.actions$.pipe(…)) is subscribed by the Effects system.

  • Listens to the Actions stream

    • this.actions$ is an injected stream of every action dispatched in your app.

    • You pipe it into RxJS operators to filter and transform.

  • Filters for the createItem action

    • ofType(ItemsActions.createItem) lets only createItem actions through.

    • It also types the payload so that downstream you can destructure { item }.

  • Performs an asynchronous side-effect

    • mergeMap(({ item }) => this.itemService.create(item).pipe(…))

      • For each incoming createItem action, it calls your HTTP service method itemService.create(item), which returns an Observable<Item>.

      • mergeMap ensures that multiple simultaneous create requests can all run in parallel (as opposed to switchMap, which would cancel previous requests).

  • Maps the HTTP result back into a new action

    • On success, .pipe(map(created => ItemsActions.createItemSuccess({ item: created })))

      • Wraps the newly created item in a createItemSuccess action, which will get dispatched automatically by NgRx Effects.

  • Handles errors by dispatching a failure action

    • .pipe(catchError(err => of(ItemsActions.createItemFailure({ error: err.message }))))

      • Catches any HTTP or network error, wraps it in a createItemFailure action, and emits that instead.


  • Full example

    1. Model & Adapter

    ts
    // items.model.ts export interface Item { id: string; name: string; description: string; } // items.adapter.ts import { createEntityAdapter, EntityState } from '@ngrx/entity'; import { Item } from './items.model'; export const itemAdapter = createEntityAdapter<Item>(); export interface ItemsState extends EntityState<Item> { loading: boolean; error: string | null; } // Initialize with empty collection + flags export const initialItemsState: ItemsState = itemAdapter.getInitialState({ loading: false, error: null, });

    2. Actions

    ts
    // items.actions.ts import { createAction, props } from '@ngrx/store'; import { Item } from './items.model'; // Create flow export const createItem = createAction('[Items] Create Item', props<{ item: Partial<Item> }>()); export const createItemSuccess = createAction('[Items API] Create Item Success', props<{ item: Item }>()); export const createItemFailure = createAction('[Items API] Create Item Failure', props<{ error: string }>()); // Get flow (will check cache first) export const getItem = createAction('[Items] Get Item', props<{ id: string }>()); export const getItemSuccess = createAction('[Items API] Get Item Success', props<{ item: Item }>()); export const getItemFailure = createAction('[Items API] Get Item Failure', props<{ error: string }>()); // Clear all export const clearItems = createAction('[Items] Clear All Items');

    3. Reducer

    ts
    // items.reducer.ts import { createReducer, on } from '@ngrx/store'; import { itemAdapter, initialItemsState, ItemsState } from './items.adapter'; import * as ItemsActions from './items.actions'; export const itemsReducer = createReducer<ItemsState>( initialItemsState, // — Create on(ItemsActions.createItem, state => ({ ...state, loading: true, error: null })), on(ItemsActions.createItemSuccess, (state, { item }) => itemAdapter.addOne(item, { ...state, loading: false }) ), on(ItemsActions.createItemFailure, (state, { error }) => ({ ...state, loading: false, error, })), // — Get (success upserts, so it adds or updates) on(ItemsActions.getItem, state => ({ ...state, loading: true, error: null })), on(ItemsActions.getItemSuccess, (state, { item }) => itemAdapter.upsertOne(item, { ...state, loading: false }) ), on(ItemsActions.getItemFailure, (state, { error }) => ({ ...state, loading: false, error, })), // — Clear on(ItemsActions.clearItems, state => itemAdapter.removeAll({ ...state, loading: false, error: null }) ) );

    4. Selectors

    ts
    // items.selectors.ts import { createFeatureSelector, createSelector } from '@ngrx/store'; import { itemAdapter, ItemsState } from './items.adapter'; export const selectItemsState = createFeatureSelector<ItemsState>('items'); const { selectAll: selectAllItems, selectEntities: selectItemEntities, selectIds: selectItemIds, selectTotal: selectItemsCount, } = itemAdapter.getSelectors(selectItemsState); export { selectAllItems, selectItemEntities, selectItemIds, selectItemsCount, }; // single-item selector factory export const selectItemById = (id: string) => createSelector(selectItemEntities, entities => entities[id]);

    5. Effects (with cache-check)

    ts
    // items.effects.ts import { Injectable } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { Store } from '@ngrx/store'; import * as ItemsActions from './items.actions'; import { ItemService } from './items.service'; import { selectItemEntities } from './items.selectors'; import { mergeMap, map, catchError, take, switchMap } from 'rxjs/operators'; import { of } from 'rxjs'; @Injectable() export class ItemsEffects { constructor( private actions$: Actions, private store: Store, private itemService: ItemService ) {} // Create: POST → success/failure createItem$ = createEffect(() => this.actions$.pipe( ofType(ItemsActions.createItem), mergeMap(({ item }) => this.itemService.create(item).pipe( map(created => ItemsActions.createItemSuccess({ item: created })), catchError(err => of(ItemsActions.createItemFailure({ error: err.message })) ) ) ) ) ); // Get: check cache, else GET → upsert or failure getItem$ = createEffect(() => this.actions$.pipe( ofType(ItemsActions.getItem), // for each getItem, grab current entities once switchMap(({ id }) => this.store.select(selectItemEntities).pipe( take(1), // only one emission mergeMap(entities => { const cached = entities[id]; if (cached) { // already in store → emit success immediately return of(ItemsActions.getItemSuccess({ item: cached })); } // not found → fetch from API return this.itemService.get(id).pipe( map(item => ItemsActions.getItemSuccess({ item })), catchError(err => of(ItemsActions.getItemFailure({ error: err.message })) ) ); }) ) ) ) ); }

    6. HTTP Service

    ts
    // items.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Item } from './items.model'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ItemService { constructor(private http: HttpClient) {} create(item: Partial<Item>): Observable<Item> { return this.http.post<Item>('/api/items', item); } get(id: string): Observable<Item> { return this.http.get<Item>(`/api/items/${id}`); } }

    7. Module Registration

    ts
    // items.module.ts import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { itemsReducer } from './items.reducer'; import { EffectsModule } from '@ngrx/effects'; import { ItemsEffects } from './items.effects'; @NgModule({ imports: [ StoreModule.forFeature('items', itemsReducer), EffectsModule.forFeature([ItemsEffects]), ], }) export class ItemsModule {}

    8. Component Usage

    ts
    // items.component.ts import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import * as ItemsActions from './items.actions'; import * as ItemsSelectors from './items.selectors'; import { Observable } from 'rxjs'; import { Item } from './items.model'; @Component({ /* ... */ }) export class ItemsComponent { loading$: Observable<boolean>; allItems$ = this.store.select(ItemsSelectors.selectAllItems); // get one by ID itemById$(id: string): Observable<Item|undefined> { return this.store.select(ItemsSelectors.selectItemById(id)); } constructor(private store: Store) { this.loading$ = this.store.select(state => state.items.loading); } addNew() { const newItem = { name: 'Foo', description: 'Bar' }; this.store.dispatch(ItemsActions.createItem({ item: newItem })); } loadOne(id: string) { this.store.dispatch(ItemsActions.getItem({ id })); } clearAll() { this.store.dispatch(ItemsActions.clearItems()); } }

    Flow summary

    1. Dispatch getItem({ id }).

    2. Effect checks selectItemEntities once:

      • If found, it emits getItemSuccess({ item: cached }) → reducer upserts (no-op change).

      • If not, it calls /api/items/:id → on success emits getItemSuccess({ item }) → reducer upserts new item.

    3. Reducer on success runs itemAdapter.upsertOne(...), merging into the normalized store.

    This ensures you only hit the API when you truly need to, and always keep your cache in sync via the Entity Adapter.


    angular NGRX 2025 action reducer effects

     

    • Actions describe events.

    • Reducers update state synchronously and purely.

    • Effects handle asynchronous or other side-effect work, bridging Actions → external APIs → new Actions.

    This separation of concerns makes your NgRx store highly predictable, testable, and traceable.


  • Component calls


    store.dispatch(loadTodos());
  • Actions stream emits a loadTodos action.

  • Reducer sees loadTodos → updates state to { loading: true }.

  • Effect sees loadTodos, performs HTTP call, then dispatches either

    • loadTodosSuccess({ todos })

    • or loadTodosFailure({ error })

  • Reducer handles the success/failure action → stores data or error, sets loading: false.

  • Component selects slices of state via store.select(...) to render UI.

  • ---------------------

  • auth effects listening on auth action

  • auth effect emit component based action

  • component based auction reigster auction

  • component based reducer listen on auction(synchorous) -> calls entity dapter to remove data

  • ------------------------------



  • Thursday, 16 November 2023

    Angular redux with NGRX

     Angular redux (state management)


    action/effects/reducer/store(state,selector)


    offcial documentation :https://ngrx.io/guide/store/selectors


    version (https://ngrx.io/docs):

    Version 14 has the minimum version requirements:

    Angular version 14.x

    Angular CLI version 14.x

    TypeScript version 4.6.x

    RxJS version ^6.5.3 || ^7.5.0


    Version 12 has the minimum version requirements:

    Angular version 12.x

    Angular CLI version 12.x

    TypeScript version 4.2.x

    RxJS version 6.5.x

    V7 has the minimum version requirements:

    Angular version 7

    TypeScript version 3.1.x

    RxJS version 6.x


    actions

    https://ngrx.io/guide/store/actions

    Action Interface


    interface Action {

      type: string;

    }


    login-page.actions.ts

    import { createAction, props } from '@ngrx/store';


    export const login = createAction(

      '[Login Page] Login',

      props<{ username: string; password: string }>()

    );


    login-page.component.ts

    onSubmit(username: string, password: string) {

      store.dispatch(login({ username: username, password: password }));

    }



    https://codeburst.io/angular-10-ngrx-store-by-example-333cbf16862c

    * Action can just be dispatched as an object



    // src/app/product/product.component.ts

    import { Product } from './product.model';

    import { AppState } from './../app.state';

    import { Component, OnInit } from '@angular/core';

    import { Store } from '@ngrx/store';

    @Component({

      selector: 'app-product',

      templateUrl: './product.component.html',

      styleUrls: ['./product.component.css']

    })

    export class ProductComponent implements OnInit {

      products: Observable<Product[]>;

      constructor(private store: Store<AppState>) {

    this.products = this.store.select(state => state.product);

       }

      addProduct(name, price) {

    this.store.dispatch({

      type: 'ADD_PRODUCT',

      payload: <Product> {

    name: name,

    price: price

      }

    });

      }

      ngOnInit() {

      }

    }



    reducers

    https://ngrx.io/guide/store/reducers

    *handles action, save the state in store 


    scoreboard-page.actions.ts


    import { createAction, props } from '@ngrx/store';


    export const homeScore = createAction('[Scoreboard Page] Home Score');

    export const awayScore = createAction('[Scoreboard Page] Away Score');

    export const resetScore = createAction('[Scoreboard Page] Score Reset');

    export const setScores = createAction('[Scoreboard Page] Set Scores', props<{game: Game}>());


    scoreboard.reducer.ts


    import { Action, createReducer, on } from '@ngrx/store';

    import * as ScoreboardPageActions from '../actions/scoreboard-page.actions';


    export interface State {

      home: number;

      away: number;

    }

    export const initialState: State = {

      home: 0,

      away: 0,

    };

    export const scoreboardReducer = createReducer(

      initialState,

      on(ScoreboardPageActions.homeScore, state => ({ ...state, home: state.home + 1 })),

      on(ScoreboardPageActions.awayScore, state => ({ ...state, away: state.away + 1 })),

      on(ScoreboardPageActions.resetScore, state => ({ home: 0, away: 0 })),

      on(ScoreboardPageActions.setScores, (state, { game }) => ({ home: game.home, away: game.away }))

    );

    !!! Register reducer, reducer is registered in store

    app.module.ts

    import { NgModule } from '@angular/core';

    import { StoreModule } from '@ngrx/store';

    import { scoreboardReducer } from './reducers/scoreboard.reducer';


    @NgModule({

      imports: [

    StoreModule.forRoot({ game: scoreboardReducer })

      ],

    })

    export class AppModule {}

    --------------------

    register reducer as feature :

    !!! Register reducer, reducer is registered in store


    app.module.ts


    import { NgModule } from '@angular/core';

    import { StoreModule } from '@ngrx/store';


    @NgModule({

      imports: [

    StoreModule.forRoot({})

      ],

    })

    export class AppModule {}

    scoreboard.reducer.ts


    export const scoreboardFeatureKey = 'game';

    scoreboard.module.ts


    import { NgModule } from '@angular/core';

    import { StoreModule } from '@ngrx/store';

    import { scoreboardFeatureKey, scoreboardReducer } from './reducers/scoreboard.reducer';

      

    @NgModule({

      imports: [

    StoreModule.forFeature(scoreboardFeatureKey, scoreboardReducer)

      ],

    })

    export class ScoreboardModule {}


    stores(state, selectors)


    https://codeburst.io/angular-10-ngrx-store-by-example-333cbf16862c


    1) define state :


    src/app/app.state.ts

    // src/app/app.state.ts

    import { Product } from './product/product.model';

    export interface AppState {

      readonly product: Product[];

    }

    2) Import state and store in component 

    (!!! reducer is registered in store at app module)

    // src/app/product/product.component.ts

    import { Product } from './product.model';

    import { AppState } from './../app.state';

    import { Component, OnInit } from '@angular/core';

    import { Store } from '@ngrx/store';

    @Component({

      selector: 'app-product',

      templateUrl: './product.component.html',

      styleUrls: ['./product.component.css']

    })

    export class ProductComponent implements OnInit {

      products: Observable<Product[]>;

      constructor(private store: Store<AppState>) {

    this.products = this.store.select(state => state.product);

       }



    selectors:

    https://ngrx.io/guide/store/selectors

    (to select some piece of state instead of returning all state)

    index.ts


    import { createSelector } from '@ngrx/store';

     

    export interface User {

      id: number;

      name: string;

    }

     

    export interface Book {

      id: number;

      userId: number;

      name: string;

    }

     

    export interface AppState {

      selectedUser: User;

      allBooks: Book[];

    }

     

    export const selectUser = (state: AppState) => state.selectedUser;

    export const selectAllBooks = (state: AppState) => state.allBooks;

     

    export const selectVisibleBooks = createSelector(

      selectUser,

      selectAllBooks,

      (selectedUser: User, allBooks: Book[]) => {

    if (selectedUser && allBooks) {

      return allBooks.filter((book: Book) => book.userId === selectedUser.id);

    } else {

      return allBooks;

    }

      }

    );


    addtional selector example (https://www.codemag.com/article/1811061/Angular-and-the-Store)

    export const getAllDevelopers =

    createSelector(getState, (state): Developer[] => {

    return state && state.developers;

    }

    );

    this.store.select<Developer[]>(getAllDevelopers).subscribe(

    developers => console.log(developers)

    );


    effects 


    https://v7.ngrx.io/guide/effects


    !!! effects just pipe on action, do an api call, map the result into new action to handle by reducer !!!!


    // Without effects 

    movies-page.component.ts


    @Component({

      template: `

    <li *ngFor="let movie of movies">

      {{ movie.name }}

    </li>

      `

    })

    export class MoviesPageComponent {

      movies: Movie[];

     

      constructor(private movieService: MoviesService) {}

     

      ngOnInit() {

    this.movieService.getAll().subscribe(movies => this.movies = movies);

      }

    }


    movies.service.ts


    @Injectable({

      providedIn: 'root'

    })

    export class MoviesService {

      constructor (private http: HttpClient) {}


      getAll() {

    return this.http.get('/movies');

      }

    }

    // with effects 

    movies-page.component.ts


    @Component({

      template: `

    <div *ngFor="let movie of movies$ | async">

      {{ movie.name }}

    </div>

      `

    })

    export class MoviesPageComponent {

      movies$: Observable = this.store.select(state => state.movies);

      constructor(private store: Store<{ movies: Movie[] >}) {}

      ngOnInit() {

    this.store.dispatch({ type: '[Movies Page] Load Movies' });

      }

    }

    movie.effects.ts


    import { Injectable } from '@angular/core';

    import { Actions, Effect, ofType } from '@ngrx/effects';

    import { of } from 'rxjs';

    import { map, mergeMap } from 'rxjs/operators';

     

    @Injectable()

    export class MovieEffects {

     

      @Effect()

      loadMovies$ = this.actions$

    .pipe(

      ofType('[Movies Page] Load Movies'),

      mergeMap(() => this.moviesService.getAll()

    .pipe(

      map(movies => ({ type: '[Movies API] Movies Loaded Success', payload: movies })),

      catchError(() => of({ type: '[Movies API] Movies Loaded Error' }))

    ))

      )

    );

     

      constructor(

    private actions$: Actions,

    private moviesService: MoviesService

      ) {}

    }


    Register effects at root 


    app.module.ts


    import { EffectsModule } from '@ngrx/effects';

    import { MovieEffects } from './effects/movie.effects';


    @NgModule({

      imports: [

    EffectsModule.forRoot([MovieEffects])

      ],

    })

    export class AppModule {}