Search code examples
angularngrx

NGRX Preventing an Infinite Loop Using CombineLatest


My Angular project has an NGRX Store which holds the state for plan. My plan object includes an array of locations called locations. I subscribe to an RXJS combineLatest to watch for changes to several values including locations. If any of the values change I perform various calculations and need to save the updated locations to the NGRX Store.

I cannot prevent an infinite cycle whereby a value in my combineLatest changes, calculations are performed, a new plan is created and a new array of locations are created named updatedLocations (as these are both immutable), I update the values in some of the locations, updatedPlan.locations = updatedLocations, and finally dispatch an action to update plan in the Store. However my combineLatest sees that plan.locations has changed and the whole process starts again.

A summary of the code is as follows:

  private locations$: Observable<ILocation[]> = this.store.pipe(select(fromPlan.getLocations)).pipe(distinctUntilChanged());
  private foo$...
  private bar$...

    this.locationSubscription = combineLatest(
      this.locations$,
      this.foo$,
      this.bar$
    ).subscribe(
      ([locations, foo, bar]) => {
        // Create new Locations so I can change values.
        const updatedLocations = [...locations.map(l => ({ ...l }))]

        // Perform calculations

        // Update the Plan
        const updatedPlan = { ...this.plan };
        updatedPlan.locations = updatedLocations;
        this.store.dispatch(new planActions.SetPlan(updatedPlan));

      }
    }

Any suggestions would be really appreciated.


Solution

  • You have this problem because you are storing derived state which is an antipattern - see 'Duplicate/derived state' at https://medium.com/@m3po22/stop-using-ngrx-effects-for-that-a6ccfe186399

    Essentially, updatedLocations is derived from other state, so you should:

    • store locations, foo, bar in your state as you do now
    • create a selector which calculates the updated locations from those 3 lists
    const selectUpdatedLocations = createSelector(
        selectLocations,
        selectFoo,
        selectBar,
        calculateUpdatedLocations
    );
    
    function calculateUpdatedLocations(locations, foo, bar){
         // Create new Locations so I can change values.
         const updatedLocations = [...locations.map(l => ({ ...l }))];
         return updatedLocations;
    }
    

    I suspect something similar will apply to Plan- ie: use a selector to build up the Plan from information in the state and selectUpdatedLocations, rather than storing the Plan itself.