0
votes

Angular v10, NGRX v10

I have a Plan object and one of its properties is an array of Task objects. In my TaskService I make a deep clone (lodash) of my Plan object, update some of the properties in my Tasks and then dispatch an Action to update the Plan in my Store. Curiously when I console.log(action.payload) in my Reducer the Task properties have reverted back to their original values! Four days looking at this and I just can't see it.

The process is started by dragging a Task on a Gantt Chart after which TaskService.updateTasks(tasksToUpdate) is called passing any Tasks which have changed following user interaction.

I have created a hugely simplified version of my updateTasks method in my TaskService and I still get the same problem. My simplified updateTasksSimple method is as follows:

  public updateTasksSimple(tasks: ITask[]): void {

    const updatedPlan: IPlan = this.coreDataService.cloneDeep(this.currentPlanWithChildren);
    const updatedTasks: ITask[] = this.coreDataService.cloneDeep(tasks);
    const tasksToUpdate = updatedTasks.filter(t => t.state !== objectState.Unchanged);

    // Check we have a tasksToUpdate object.
    if (tasksToUpdate !== null) {

      // Ensure we have some Tasks to update.
      if (tasksToUpdate.length > 0) {

        // Logging
        tasksToUpdate.forEach(t => {
          if (t.state !== objectState.Unchanged) {
            console.log(`Updating Task - ${t.title}, Summary: ${t.summary}, Start: ${t.start}, End: ${t.end}, Duration: ${t.durationHours}, Constraint: ${t.taskConstraintType.name}`);
          }
        });

        // Map updated tasks
        updatedPlan.tasks = tasksToUpdate.map((t: ITask) => ({
          ...t,
          start: new Date(2021, 2, 26, 16),
          end: new Date(2021, 2, 26, 22),
          durationHours: 6,
        }));

        // Logging
        console.log(`updateTasksSimple Tasks before dispatch:`);
        updatedPlan.tasks.forEach(t => {
          console.log(`${t.title}, start: ${t.start}, end: ${t.end}, durationHours: ${t.durationHours}, constraint: ${t.taskConstraintType.name}`);
        })

        // Dispatch an Action to update the Store.
        this.store.dispatch(new planActions.SetCurrentPlanWithChildren(updatedPlan));

      }

    }

  }

My Action is defined as follows:

export class SetCurrentPlanWithChildren implements Action {
  readonly type = PlanActionTypes.SetCurrentPlanWithChildren;
  constructor(public payload: IPlan) { }
}

I dispatch my Action as follows:

  private dispatchUpdate() {

    // Logging
    console.log(`dispatchUpdate called. Tasks:`);
    this.updatedPlan.tasks.forEach(t => {
      console.log(`${t.title}, start: ${t.start}, end: ${t.end}, durationHours: ${t.durationHours}, constraint: ${t.taskConstraintType.name}`);
    })

    this.store.dispatch(new planActions.SetCurrentPlanWithChildren(this.updatedPlan));

  };

As you can see I am temporarily logging the Tasks to see exactly what I am dispatching from my TaskService and everything is fine here.

My Reducer is as follows:

    case PlanActionTypes.SetCurrentPlanWithChildren:
      console.log(`PlanActionTypes.SetCurrentPlanWithChildren.`, action.payload);
      return {
        ...state,
        currentPlanWithChildren: action.payload,
        error: ''
      };

The payload logged here has the original values before my TaskService made changes. As a consequence of this when I navigate to a component which subscribes to the following Selector:

export const getCurrentPlanWithChildren = createSelector(
  getPlanFeatureState,
  (state: PlanState) => state.currentPlanWithChildren
);

it doesn't display the updated values. There's a good chance I'm missing something obvious here, but I can't see it. Any advice or suggestions extremely welcome.

Update On further investigation I observed the following: My app has 2 pages; Gantt and Tasks. If I navigate to the Gantt page and call my updateTasksSimple method it changes the start, end and duration properties on my Tasks and dispatches an Action to update the Store, as detailed above. If I then navigate to my Tasks page which subscribes to the Store the initial Tasks received by the observable are the old values. However, if I reload the app, navigate to the Gantt page and call my updateTasksSimple method TWICE then navigate to the Tasks page and look at my console.log (which uses rxjs tap on the observable) the initial Tasks received are the new values. It's as if the initial values received by my observable are old values, not the current values in the Store. My Tasks page declares my observable as follows:

  ...
  public currentPlanWithChildren$: Observable<IPlan>;
  ...

  ngOnInit() {

    // Log the Tasks when the CurrentPlanWithChildren changes.
    this.currentPlanWithChildren$ = this.store.pipe(select(fromPlan.getCurrentPlanWithChildren)).pipe(
      tap(currentPlanWithChildren => {
        if (currentPlanWithChildren != null) {
          console.log(`PlanTasksContainerComponent.currentPlanWithChildren changed.`);
          currentPlanWithChildren.tasks.forEach(t => {
            console.log(`Task - ${t.title}, Summary: ${t.summary}, Start: ${t.start.toString()}, End: ${t.end.toString()}, Duration: ${t.durationHours.valueOf()}, Constraint: ${t.taskConstraintType.name}`);
          })
        }
      })
    );

  }

Very odd.

2
Can you provide the TaskService code? And how and when is the dispatchAction() method called? - dallows
Good comment. I re-visited my method in my TaskService and hugely simplified it, hardcoding some dates and durations in. Still when I log the Tasks before dispatch they are as expected and yet in the Reducer they are incorrect! - TDC
That's really odd behavior. Once again, have you tried Redux Dev Tools in the web browser? You might notice some strange things happening there. - dallows

2 Answers

0
votes

What seems little bit suspicious is that even though you assign the deep copy of currentPlanWithChildren to updatedPlan but then you just assign updated tasks to it by reference. Try creating the copy of the updated tasks:

    // Map updated tasks
    updatedPlan.tasks = tasksToUpdate.map(t => ({
      ...t,
      t.start = new Date(2021, 2, 26, 16);
      t.end = new Date(2021, 2, 26, 22);
      t.durationHours = 6;
    }));
    // this is not necessary then
    // updatedPlan.tasks = tasksToUpdate;
0
votes

After much investigation I found that the problem was caused by my own poor use of NGRX. Elsewhere in my code I subscribed to a selector returning currentPlanWithChildren.tasks which is an Observable<ITask[]>. I then sorted the returned array of Tasks, mutating the Tasks object inadvertently and this appears to have been at the root of my issue. To fix it I simple changed my sort to be tasks.slice().sort(t => ...