Updating an item in the redux state is triggering a never-ending loop, leading to a browser

EXPECTED OUTCOME: My goal is to modify a value in my redux state

ISSUE: I am encountering an issue where there is an infinite loop or the browser gets locked down. Despite consulting this Stack Overflow post and the official documentation, I am struggling to identify the mistake in my approach.

This represents my current state:

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

I intend to update it to

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

The URL being accessed is: localhost:4200/#/0

I am using a selector to retrieve all items from my store, inspect the URL parameters, and return the corresponding item from the state. The provided URL will return

{ id: 0, product: TV, saleItem: false }
. I then execute
item = { ...item, saleItem: true };
within my effect and trigger the reducer. However, this leads to an endless loop with console.log('before', item); and console.log('after', item); repeatedly logging out. Below you can find the code I have implemented along with some alternate attempts I have made.

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.
  • Called the selector and passed the value into the action as payload
  • Updated the item in the reducer

This led to 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
  };
}

Answer №1

Here's why you're stuck in an endless loop:

When you subscribe to the selector, you create a new reference in the effect and make changes to the item. This triggers an update to the state which then reactivates the selectors, creating a cycle of updates.

To resolve this issue:

  • Avoid updating the item within your effect.
  • Pass the selected item as payload in the update action.
  • Update the item within your reducer without needing separate success or fail actions like in your example.

Answer №2

After making the necessary adjustments, I'm still not completely satisfied with the solution. Ideally, I would prefer to have a single line in my effects file containing both my dispatch and selector. I explored using withLatestFrom() on my selector for a neater implementation

LATEST UPDATE: As an alternative, I came across and utilized the following discussion: on gihub and stackblitz. Option 2 was applied where I replaced do with map. This allowed me to execute a single line in my component

this.store.dispatch(new fromStore.UpdateItem());
and then update the item in the reducer with
[item.id]: { ...state.entities[item.id], saleItem: true }

Initial Resolution

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 Approach 1 - replacing tap with map. A more streamlined option

@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)))
  );

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Form submission resulting in 405 error

Currently, I am facing an issue while trying to send data to the server. The PHP side seems to be functioning correctly as the post request is successful when tested using Postman. However, when attempting to do so from Angular, I encounter a 405 (Method N ...

Strange behavior in Angular's http response

When I make a call to my API and receive a JSON response, the code snippet below illustrates how I handle it: getAllLearn() { this.learnService.getAllLearn().subscribe(res =>{ // The console log shows that res.featured only has one index: ( ...

Customizing the Material UI v5 theme with Typescript is impossible

I'm attempting to customize the color scheme of my theme, but I am encountering issues with accessing the colors from the palette using theme.palette. Here is a snippet of my theme section: import { createTheme } from "@mui/material/styles&qu ...

What is the best way to include an item in a list with a specific identifier using the useState Record data type in a React application built with TypeScript?

Here is the structure of my Record type: const PEOPLE_MAP_INIT: Record<string, Person[]> = { "1": [], "2": [], "3": [] }; I have initialized the useState like this: const [PEOPLE_MAP, SET_PEO ...

Accessing a variable within a function in Angular

Recently I started working with Angular and encountered an issue while trying to access a variable inside a function. Below is the code snippet that's causing me trouble: mergeImages() { var imgurl; var canvas: HTMLCanvasElement = this.canv ...

Narrow down your search results with date range filtering in Angular Material

Seeking guidance as a newcomer to Angular Material, I am trying to implement a date range filter for a table of N results. The options in the filter select are (1 day, 5 days, 1 week, 15 days), which are populated using a variable JS vm.rangos=[ {id ...

Upgrading to the official release of Angular 2 from RC6 with webpack: a step-by-step

I have been working with rc6 in Angular from the angular2 starter class. How can I upgrade to the most recent version of Angular? Is there a command that will handle the update automatically or do I need to manually adjust the versions in package.json? ...

Dynamic setting of application URL in Spring Social at runtime

Our Application Structure: We utilize APIs such as Spring Boot, Spring Security, and Spring Social. UI-1 is powered by Angular 5 and communicates with the APIs. UI-2 also runs on Angular 5 and interacts with the same APIs. Users are authenticated via Sp ...

It is not possible to use an async function with Ionic 4 ToastController buttons

Incorporating a new function into the handler of my ToastController button to return a promise (in this case: this.navCtrl.navigateForward()) is something I need assistance with. This is what my code looks like: const toast = await this.toastController.c ...

How can I apply unique "compilerOptions" settings to a specific file in tsconfig.json file?

Can I apply specific tsconfig options to just one file? Here is my current tsconfig.json: { ... "compilerOptions": { ... "keyofStringsOnly": false, "resolveJsonModule": true, "esModuleInterop": t ...

Changes made to the updated component fields are not reflecting on the user interface

I've encountered an issue where I can't seem to update a variable in a component that is being displayed on the UI. Even though the content of the variable changes correctly, the UI fails to reflect this change. Strangely enough, when checking th ...

Mastering the Art of Sharing PrimgNg Selected Checkboxes with Child Component Dropdown

I am developing a simple application using PrimeNg. In order to pass information from the selected items of a Multi-Select component in the parent element (<p-multiSelect/>) to a Dropdown component in the child element (<p-dropdown/>), I have i ...

Top method for triggering an action on the client-side during Sign In with the help of Redux, React, and NextAuth

Currently, I am developing a web application that utilizes the Spotify API. My goal is to seamlessly load the user's playlists as soon as they log in using NextAuth. At the moment, there is a button implemented to trigger playlist loading, but it onl ...

What is the process to activate a function within a component once a service method has been run?

I'm currently working on a chart configuration using amCharts, where an eventListener is registered for the bullets. This event listener then triggers another function in my chart service. My goal is to activate a method in my component as soon as th ...

Angular 2 Template - Show alternate content if the string is empty

Back in the AngularJS days, there was a neat trick where you could bind data to a string directly in the markup like this: {{myString | 'N/A'}} This little trick would check if the string was empty and if so, display 'N/A' instead. It ...

Error occurs in Angular Mat Table when attempting to display the same column twice, resulting in the message "Duplicate column definition name provided" being

What is the most efficient method to display a duplicated column with the same data side by side without altering the JSON or using separate matColumnDef keys? Data: const ELEMENT_DATA: PeriodicElement[] = [ {position: 1, name: 'Hydrogen', wei ...

I encountered an error in my Node.js application stating that it could not find the name 'Userdetailshistory' array. I am puzzled as to why this error is occurring and I suspect it may be due to my

import { Component, OnInit } from '@angular/core'; import { UserdetailshistoryService } from '../../services'; @Component({ selector: 'my-userdetailshistory', templateUrl: './userdetails-history.component.html', ...

Issue: Actions should be in the form of plain objects. However, the given type is 'Promise'. To resolve this, consider incorporating middleware into your React Native store

Currently, I am facing an issue while trying to retrieve products from Firebase. Despite having redux-thunk installed to manage promises and using middleware in my store, I encountered the following error: Actions must be plain objects. The actual type d ...

React is not displaying the most recent value

During the initial rendering, I start with an empty array for the object date. After trying to retrieve data from an influxDB, React does not re-render to reflect the obtained results. The get function is being called within the useEffect hook (as shown in ...

The second guard in Angular 5 (also known as Angular 2+) does not pause to allow the first guard to complete an HTTP request

In my application, I have implemented two guards - AuthGuard for logged in users and AdminGuard for admins. The issue arises when trying to access a route that requires both guards. The problem is that the AdminGuard does not wait for the AuthGuard to fini ...