#NGRX updating nested objects' value

38 messages · Page 1 of 1 (latest)

foggy gale
#

Hello everyone. I'm trying to update a nested object in the NGRX Store (ignore the styling) but in the image with the red box those boxes update based on certain conditions with the item (item status, too much time in station, etc.. ) When one of these conditions is met there is a stored boolean value that changes the color of the box. The issue I'm having is that there is a number value on the station object that stores the amount of errors (red boxes) and displays it so the user can easily see how many issues a particular station is having.
The call to the reducer is

this.store.dispatch(updateStationErrors({ station: this.station, wasWarning: this.warningTrigger }));

and the state, and reducers look like this

interface Business {
 lines: Line[];
}

export interface BusinessState {
 business: Business;
}
export const initialBusinessState = {
 lines: [new Line(1)],
}
export const businessReducer = createReducer(
 initialBusinessState,
 on(BusinessActions.setLines, (state, action) => {
   return {
     lines: action.lines
   }
 }),
 on(BusinessActions.updateStationWarnings, (state, action) => {
   return state;
 }),
 on(BusinessActions.updateStationErrors, (state, action) => {
   state.lines.map((value, index) => {
     for (let i = 0; i < value.numOfSteps; i++) {
       if (value.steps[i].stationId == action.station.stationId) {
         if (action.wasWarning) {
           state.lines[index].steps[i].errors--;
         }
         state.lines[index].steps[i].warnings++;
       }
     }
   });

   return state;
 }),
);

this isn't the correct way to update state in ngrx. I've tried spread notation before but I get confused by it and when its nested it gets complicated (to me).
Action code for BusinessActions.updateStationErrors

export const updateStationErrors = createAction(
  "[Business Overview] Update station errors",
  props<{station:Station, wasWarning: boolean}>()
)
idle gale
#

@foggy gale can you share your Line class?

foggy gale
#
import {Station} from './Station';
export class Line {
  id: number;
  numOfSteps:number;
  lineName: string;
  steps: Station[];
  station1: Station;
  station2: Station;
  station3: Station;
  station4: Station;
  station5: Station;
  station6: Station;

  constructor(id: number){
    this.station1 = new Station(1,"Cut",id);
    this.station2 = new Station(2,"Sew first step",id);
    this.station3 = new Station(3,"Sew Closed",id);
    this.station4 = new Station(4,"Stuffed",id);
    this.station5 = new Station(5,"Boxed",id);
    this.station6 = new Station(6,"Shipped",id);
    this.id = id; 
    console.log(this.id);
    if (id == 1) {
      this.lineName = "Primary 'Commerce Lane' Line";
    } else {
      this.lineName = "Secondary 'Commerce Lane' Line";
    }
    this.numOfSteps = 7;
    this.steps = [this.station1,this.station2,this.station3,this.station4,this.station5,this.station6];

  }
  public getSteps(){
      return this.steps;
  }
  public getLineName(){
    return this.lineName;
  }
}
#

side note this constructor has static data right now (this will be changed later)

#

Also stations 1 - 6 won’t exist in the future it will all exist within steps and will be retrieved via api calls

idle gale
#

Are you able to change the store structure at all? As it'll be a lot easier to reason with if flattened.

foggy gale
#

I don't think it can be flattened.. can it? i'm trying to imagine what the looks like and I don't think its possible for this application

idle gale
#

Absolutely can, I will try come up with a flattened/normalised structure for you.

#

@foggy gale can you please share the Station class

foggy gale
#
import { Item } from "./Item";
import { ItemStatus } from "./ItemStatus";
import { Section } from "./Section";
export class Station{
  currentItems: Item[];
  backlogItems: Item[];
  completedItems: Item[];
  stationId: number;
  function: string;
  warnings: number;
  errors: number;
  lineId: number;
  constructor(stationId: number, station: string, lineId: number) {
    this.warnings = 0;
    this.errors = 0;
    this.stationId = stationId;
    this.function = station;
    this.lineId = lineId;
    this.backlogItems = [new Item(3, "704829-BB", "Black", ItemStatus.not_started, 123456, 6, "Flying Cloud Sofa", "Flying Cloud", "May 6, 2022 10:58", 0, 5, 10,Section.QUEUE,stationId)];
    this.currentItems = [new Item(1,"704829-BB","Black",ItemStatus.blocked,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",0,5,10,Section.IN_PROGESS,stationId),
              new Item(2,"704829-BB","Black",ItemStatus.in_progress,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",0,5,10,Section.IN_PROGESS,stationId)];
    this.completedItems = [new Item(4,"704829-BB","Black",ItemStatus.completed,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",120,130,140,Section.COMPLETED,stationId),
    new Item(5,"704829-BB","Black",ItemStatus.completed,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",0,5,10,Section.COMPLETED,stationId),
    new Item(6,"704829-BB","Black",ItemStatus.completed,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",0,5,10,Section.COMPLETED,stationId),
    new Item(7,"704829-BB","Black",ItemStatus.completed,123456,6,"Flying Cloud Sofa","Flying Cloud","May 6, 2022 10:58",0,5,10,Section.COMPLETED,stationId)];
    // this.currentItems = [];
    // this.completedItems = [];
  }
}

``` same deal here the backlogItems, currentItems, and CompletedItems will all be retrieved from api later
#
import { ItemStatus } from './ItemStatus';
import { Section } from './Section';
export class Item {
  id:number; //
  itemName: string; //
  itemColor: string; //
  //this should be turned into a enum later
  status: ItemStatus; //
  //this should be turned into a class for real data
  poNumber: number; //
  partQuantity: number; //
  itemDescription: string; //
  //this would be moved into the po class later
  creationDate: Date; //
  //this should be its own class later
  productLine: string; //
  //this needs to turn into a timer (DONT DO THAT)
  currentStepStart: string;
  //needs some sort of timer here
  //there should probably be a class to contain the ability to have many different time
  warningSeconds: number;
  timeInStation: number;
  errorSeconds: number;
  currentSection: Section;
  //Possibly add something for what customer it is
  stationId: number;
  constructor(id:number, itemName: string, itemColor: string,
    status: ItemStatus, poNumber: number, amtParts:number,
    itemDescripition: string, productLine:string, currentStepStart:string,timeInStation:number,warningSeconds:number,errorSeconds:number, currentSection: Section, stationId:number){
    this.stationId = stationId;
    this.id = id;
    this.itemName = itemName;
    this.itemColor = itemColor;
    this.status = status;
    this.poNumber = poNumber;
    this.partQuantity = amtParts;
    this.itemDescription = itemDescripition;
    this.creationDate = new Date();
    this.productLine = productLine;
    this.currentStepStart = currentStepStart;
    this.warningSeconds = warningSeconds;
    this.timeInStation = timeInStation;
    this.errorSeconds = errorSeconds;
    this.currentSection = currentSection;
  }
  public getTimeInStationMinutes(){
    return Math.floor(this.timeInStation / 60);
  }
  public getTimeInStationSeconds(){
    return this.timeInStation % 60;
  }
}
``` Here is the item code
idle gale
#

Any reason you have constructors that initialise all these "test" looking data?

foggy gale
#

Its just how I quickly developed it. No good reason honestly

idle gale
#

Ahh so just mock data?

foggy gale
#

yup

idle gale
#

Will each station have a unique id?

foggy gale
#

yes they will

idle gale
#

But can 2 Lines reference the same Station?

foggy gale
#

2 different lines can contain the same station

idle gale
#

But each line needs to track their own errors for the same station reference?
e.g. Line 1 can contain errors for Station 1 in its first step while at the same time Line 2 contains no errors for Station 1 in its first step?

foggy gale
#

Ok I had to re look at what the plan was. So it looks like the stations will not be in multiple lines. When someone logs into a machine as a station they could possible be authorized into several different stations and it would display on their machine as different for them, but the actual station themselves will only be in a specific line.

#

So to answer your question.. A station will only show up once

idle gale
#

Right. So a Station can only have 1 Line, but a Line can have many stations?

foggy gale
#

correct

idle gale
#

@foggy gale this is a very basic example, but it demonstrates normalisation by using maps instead of arrays. This allows much higher performance for look up, O(1) instead of O(n).

I'm happy to spend time explaining how this works.

This change means you won't be storing the Station in the Line class in the steps property anymore. The step property should be a list of station ids instead.

export const stationError = createAction(
  "[Business Overview] Station error",
  props<{stationID: string, wasWarning: boolean}>()
)

export interface BusinessState {
  lines: {
    [id: string]: Line;
  };
  stations: {
    [id: string]: Station;
  };
}

export const initialBusinessState = {
  lines: {},
  stations: {},
}
export const businessReducer = createReducer(
  initialBusinessState,
  on(BusinessActions.updateStationErrors, (state, action) => {
    const station = state.stations[action.stationID];

    return {
      ...state,
      stations: {
        ...action,
        [action.stationID]: {
          ...station,
          errors: action.wasWarning ? station.errors - 1 : station.errors,
          warnings: station.warnings + 1
        }
      } 
    }
  }),
);

There is still a lot of improvements that could be made here.
For 1, don't use classes in the store. Use interfaces. Classes can't be serialised to JSON (methods will be lost).

foggy gale
#

Ohh interesting. I didn’t even to think about some of the things you did here. I’ll attempt to implement this tomorrow morning. I appreciate the work you put in for this. I’ll ask some questions if I have issues with implementation

idle gale
#

No problem! 🙂 Happy to help.

foggy gale
#

ok with the new state changes when I attempt to pipe in the data from the store

<div *ngIf="lines$ | async as lines; else loading">
    <div *ngFor="let line of lines; trackBy: getLineId">
      <!-- <pre>For Debugging: {{ line | json }}</pre> -->
      <app-line-view [line] = "line"></app-line-view>
    </div>
</div>
<ng-template #loading>Loading...</ng-template>

I was using this previously. Obviously I need to make some adjustments to it or change it completely
The error reads Type '{ [id: string]: Line; }' is not assignable to type 'NgIterable<Line> | null | undefined'.
when hovering over the of on the second line above.

for reference

export class OverviewPage implements OnInit {

  public readonly lines$!: Observable<{[id: string]: Line; }>;

  constructor(private store: Store<BusinessState>) {

    // const lines = [new Line(1), new Line(2)];

    // this.store.dispatch(setLines({ lines }));

    this.lines$ = this.store.pipe(
      map((state: BusinessState): { [id: string]: Line; } => state.lines),
    );
  }
  ngOnInit(): void { }
  public getLineId(_: number, item: Line): string {
    return item.id.toString();
  }
  getKeys(map: any[]){
    return Array.from(map.keys());
  }
}
 

this is the class that is using that html to generate the lines

idle gale
#

@foggy gale this is where NGRX selectors are useful.

Create a new .selectors.ts file. So maybe something like business.selectors.ts.

In that selectors file, we will create some basic selectors in order to be able to select lines from your BusinessState.

// The AppState interface is the state of your Root Reducer.
// This selects the business store slice. So `state.business` is the state returned from the businessReducer.
export const selectBusinessState = (state: AppState) => {
  return state.business;
};

// Select business lines as an array instead of an object map.
export const selectBusinessLines = createSelector(selectBusinessState, state => state.lines.reduce((lines, line) => ({...lines, [line.id]: line})), {} as Line[])

And using it in your component is now like how you originally used it, but simpler.

export class OverviewPage implements OnInit {

  public readonly lines$: Observable<Line[]>;

  constructor(private store: Store<AppState>) {

    // const lines = [new Line(1), new Line(2)];

    // this.store.dispatch(setLines({ lines }));

    // Much simpler now!
    this.lines$ = this.store.pipe(
      select(selectBusinessLines),
    );
  }
  ngOnInit(): void { }
  public getLineId(_: number, item: Line): string {
    return item.id.toString();
  }
  getKeys(map: any[]){
    return Array.from(map.keys());
  }
}

Using Selectors is really powerful, as they have memoization. It may seem expensive to build an array every time you select lines, but it'll only rebuild the array if the lines have changed at all (added or removed).

foggy gale
#

I'm having issue using the reduce function on lines @idle gale

idle gale
#

Sorry about that. state.lines is a map, not an array.

export const selectBusinessLines = createSelector(selectBusinessState, state => Object.keys(state.lines).reduce((lines, lineID) => ({...lines, [line.id]: state.lines[lineID]})), [] as Line[])

Edit: Made another fix ^

#

Wait up, I need to redo this. It's so wrong haha sorry

#
export const selectBusinessLines = createSelector(selectBusinessState, state => Object.keys(state.lines).map(lineID => state.lines[lineID]))

There, fixed. Much simpler.

foggy gale
#

@idle gale What would the proper way to make a selector for getting a specific group of stations? For instance lets say I have a line whose stepIds property is ["1","2","3"] how do I get those specifically in ngrx?

idle gale
#

@foggy gale like this:

export const selectBusinessStationEntities = createSelector(
  selectBusinessState,
  (state) => state.stations
);

export const selectLine = (lineID: string) =>
  createSelector(selectBusinessState, (state) => state.lines[lineID]);

export const selectBusinessLineStations = (lineID: string) =>
  createSelector(selectBusinessStationEntities, selectLine(lineID), (stations, line) =>
    line.steps.map((stationID) => stations[stationID])
  );

Edit: Updated to take advantage of more memoization by adding a selectBusinessLineEntities selector.
Edit 2: Updated again as I got station and line backwards.

#

And then using it would be like so:

this.store.pipe(
  select(selectBusinessLineStations('1')
);
foggy gale
#

Ohh nice!! that seems to be working well. I'm going to make sure the original functionality that this post was about is met and I'll let you know how that goes 🙂

idle gale
#

Awesome! 😄