0
votes

EXPECTED OUTCOME: I'm trying to update a value on my redux state

ISSUE: I'm ending up with an infinite loop / browser locking down. I've read this 'SO' post and the docs but struggling to see where I'm going wrong.

This is my state:

{ id: 0, product: TV, saleItem: false },
{ id: 1, product: Fridge, saleItem: false }

and I want to update it to

{ id: 0, product: TV, saleItem: true }
{ id: 1, product: Fridge, saleItem: false }

my url is: localhost:4200/#/0

I'm using a selector to get all the items within my store, check the url params and return the item in the state. The above url will return { id: 0, product: TV, saleItem: false } I then run item = { ...item, saleItem: true }; within my effect an fire off the reducer. However this is causing an infinite loop somewhere with console.log('before', item); and console.log('after', item); being logged out over and over again. Below is the code I have and some alternatives I've tried

Selector

export const getBasketEntities = createSelector(
  getBasketState,
  fromItems.getBasketEntities
);

export const getSelectedItem = createSelector(
  getBasketEntities,
  fromRoot.getRouterState,
  (entities, router): Item => {
    return router.state && entities[router.state.params.id];
  }
);

Component

this.store.dispatch(new fromStore.UpdateItem());

Action

export class UpdateItem implements Action {
  readonly type = UPDATE_ITEM;
  constructor() {}
}

Effects

// update effect
@Effect()
updateItem$ = this.actions$.ofType(itemActions.UPDATE_ITEM).pipe(
  switchMap(() => {
    return this.store.select(fromSelectors.getSelectedItem).pipe(
      map((item: Item) => {
        console.log('before', item);
        item = { ...item, saleItem: true };
        console.log('after', item);
        return new itemActions.UpdateItemSuccess(item);
      }),
      catchError(error => of(new itemActions.UpdateItemFail(error)))
    );
  })
);

Reducer

case fromItems.UPDATE_ITEM_SUCCESS: {
  const item: Item = action.payload;
  console.log('reducer', item);

  const entities = {
    ...state.entities,
    [item.id]: item
  };

  return {
    ...state,
    entities
  };
}

UPDATE:

  • Removed the selector from the effect.
  • Calling the selector and passing the value into the action (payload)
  • Updating the item in the reducer

This results in the same outcome.

Component

onUpdate() {
  this.store
    .select(fromStore.getSelectedItem)
    .pipe(
      map((item: Item) => {
        this.store.dispatch(new fromStore.UpdateItem(item));
      })
    )
    .subscribe()
    .unsubscribe();

}

Effect

@Effect()
  updateItem$ = this.actions$.ofType(itemActions.UPDATE_ITEM).pipe(
  map((action: itemActions.UpdateItem) => action.payload),
  map((item: Item) => {
    return new itemActions.UpdateItemSuccess(item);
  }),
  catchError(error => of(new itemActions.UpdateItemFail(error)))
);

Action

export class UpdateItem implements Action {
  readonly type = UPDATE_ITEM;
  constructor(public payload: Item) {}
}

Reducer

case fromItems.UPDATE_ITEM_SUCCESS: {
  const item: Item = action.payload;

  const entities = {
    ...state.entities,
    [item.id]: { ...item, saleItem: true }
  };
  return {
    ...state,
    entities
  };
}
2

2 Answers

0
votes

The reason why you have an infinite loop:

You subscribe on the selector -> in the effect you create a new ref and modify the item -> you update the state -> this triggers the selectors -> and so on …

To fix this:

  • Do not update an item inside your effect.
  • Set the selected item as payload in the update action
  • Update the item in your reducer (you don't need the success or fail action in your example)
0
votes

Fixed with the below, but this doesn't feel good at all. Preferable I'd just like one line with my dispatch and selector in my effects file. I've looked into withLatestFrom() on my selector for a cleaner solution

UPDATE: Alternatively I found and used the following discussion: on gihub and stackblitz. I used Option 2 and replaced do with map. This means i can run my one line in my component this.store.dispatch(new fromStore.UpdateItem()); and then set the item replacement in the reducer with [item.id]: { ...state.entities[item.id], saleItem: true }

Initial Fix

this.selectedItem: Item;

onUpdate() {
   this.store
     .select(fromStore.getSelectedItem)
     .subscribe((item: Item) => this.selectedItem = item )
     .unsubscribe();
   this.store.dispatch(new fromStore.UpdateItem(this.selectedItem));
 }

StackBlitz method 1 - replacing tap with map. Cleaner

@Effect()
  updateItem$ = this.actions$.ofType(itemActions.UPDATE_ITEM).pipe(
    withLatestFrom(this.store.pipe(select(fromSelectors.getSelectedItem))),
    map(([type, item]: Array<any>) => {
      console.log('in effect', item);
      return new itemActions.UpdateItemSuccess(item);
    }),
    catchError(error => of(new itemActions.UpdateItemFail(error)))
  );