Search code examples
angularngrxngrx-store

NgRx Reducer - Change State of a specific object in an array


I am trying to follow the NgRx walkthrough but adapt it to something a bit more complex than adding or removing books.

NgRx Walkthrough

I want to have an array of University Objects, and be able to find a university by Id then change it's total_student number.

I am nearly there. Completely stuck with the reducers.

Models/univeristy.model.ts:

export interface University {

    id: string | undefined;
    name: string | undefined;
    total_students: number | undefined;
}

Actions/university.actions.ts:

import { createActionGroup, props } from '@ngrx/store';
import { University } from '@models/university';

export const UniversityActions = createActionGroup({
  source: 'University',
  events: {
    'Change Student Total': props<{ universityId: string, total_students: number }>(),
  },
});

export const UniversityApiActions = createActionGroup({
  source: 'University API',
  events: {
    'Retrieved Universities': props<{ universities: Array<University> }>(),
  },
});

Actions/action-types.ts:

import * as ActionTypes from './university.actions';

    export {ActionTypes};

I am losing my way here...

Reducers/collection.reducer.ts :

import { createReducer, on } from '@ngrx/store';
import { UniversityActions } from '@actions/actions/university.actions';

export const initialState: Array<string> = [];


export const collectionReducer = createReducer(
  initialState,
  on(UniversityActions.changeStudentTotal, (state, { universityId, total }) => {
    
    // How to modify the state to change the total of a specific univeristy in the list?
    // Find the univeristy by universityId then create a new record for it with the new total?
    ...state,
    universities: { ...state.universities, universities: {id: universityId, total: total} },

  })

);

Reducers/university.reducer.ts :

import { createReducer, on } from '@ngrx/store';
import { ActionTypes } from '@state/actions/action-types';
import { University } from '@models/university.model';

export interface UniversityState {
  universities: University[];
  status: 'start' | 'loading' | 'error' | 'success';
  error: string;
}

// The starting global values
export const initialState: Array<University> = [];

export const universityReducer = createReducer(
  initialState,
  on(
    ActionTypes.UniversityApiActions.retrievedUniversities,
    (_state, { universities }) => universities
  )
);

Selectors/university.selectors.ts:

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { University } from '@models/university.model';

export const selectUniversity = createFeatureSelector<Array<University>>('universities');

export const selectUniversityState =
  createFeatureSelector<Array<string>>('collection');

export const selectUniversities = createSelector(
  selectUniversity,
  selectUniversityState,
  (universities, collection) => {
    return collection.map((id) => universities.find((university) => university.id === id)!);
  }
);

Any help appreciated!


Solution

  • Given the id, you would need to search for the university and replace its data without mutating the store content. You could do it like the following:

    on({...}),
    on(UniversityActions.changeStudentTotal, (state, {universityId, total}) => {
      const universityCopy = [...state.universities]; // create a copy of the original, 
      const index = universityCopy.findIndex(university => university.id === univertisyId);
      // you should add a validation if "index" is -1 if it doesn't find any
      const searchedUniversity = {...universityCopy[index]} as University;
      
      // Modify the copy as you need
      searchedUniversity.total_students = total;
      universityCopy[index] = {...searchUniversity} as University;
      state.universities = [...universityCopy]; // give back to "universities" a 'new' array
      return { ...state };
    }),
    on({...})
    

    Since you are using NgRx, you could also make use of NgRx Entity for CRUD operations (Create, Retrieve, Update, Delete), instead of passing in the university id you should instead pass in a university object

    on(UniversityActions.changeStudentTotal, (state, {theModifiedUniversityObject}) => 
     ({...state, universities: adapter.updateOne(theModifiedUniversityObject, state.universities)}))
    

    There are also available operations such as:

    • updateMany
    • upsertOne (either Add or Update)
    • upsertMany
    • addOne
    • deleteOne
    • and many more...

    For more information about NgRx Entity you can read the official docs