LiteralMetaphore
LiteralMetaphore

Reputation: 661

How to avoid re-render of components made by looping through store object when a new action is dispatched

Sorry for the confusing title, but the problem is hard to describe so briefly. I have a service that connects to a WebSocket API and dispatches new data to an NgRx store, like so:

this.priceSocket.subscribe(
          message => this.handleMessage(message),
          error => console.log(error),
          () => console.log("completed")
        )

This is the handleMessage() function I pass the message to:

handleMessage(message) {
    if (message.price) {
      this.store.dispatch({type: "PRICES", payload: {product_id: message.product_id, price: message.price}})
    }
  }

And here is my very simple reducer:

export function priceReducer(state: object = {prices: {}}, action: Action) {
  switch (action.type) {
    case "PRICES":
      return {...state, prices: {...state.prices, [action.payload.product_id]: action.payload.price}};
  }
}

Then, in one of my components, I use .pipe(share()) on a slice of the data from the store, pass it to the template using the | async pipe and loop through it to render a child component for every property in the prices object:

prices = this.store.select(state => state.message ? state.message.prices : null).pipe(share());

And the template:

<div class="card-wrapper">
  <app-price *ngFor="let item of prices | async | keyvalue" [priceKey]="item?.key" [priceValue]="item?.value"></app-price>
</div>

This works, as it displays the data in the child components in real-time, however the issue that I've found is that all of the child components get re-rendered every time new data comes from the WebSocket and gets sent to the store. I assume this is because the store gets rebuilt every time a new action is dispatched, so *NgFor loops through the entire new object again and re-renders every child component.

I've managed to avoid this behavior by using .subscribe() on the same slice of data, building a new object from the data in the component itself that has a new BehaviorSubject for every real-time value that I want to display. I then pass the object to the template and use | async on each individual value in the object instead of the entire object. This way I get the expected behavior, which is only the relevant child component re-rendering instead of all of them. I'm wondering if the same behavior can be achieved without making this object in the component and instead passing the data directly from the store to the template.

Upvotes: 2

Views: 1120

Answers (1)

vatz88
vatz88

Reputation: 2452

When using ngFor you can also use ngForTrackBy

A function that defines how to track changes for items in the iterable.

When items are added, moved, or removed in the iterable, the directive must re-render the appropriate DOM nodes. To minimize churn in the DOM, only nodes that have changed are re-rendered.

By default, the change detector assumes that the object instance identifies the node in the iterable. When this function is supplied, the directive uses the result of calling this function to identify the item node, rather than the identity of the object itself.

The function receives two inputs, the iteration index and the node object ID.

Ref: https://angular.io/api/common/NgForOf

Upvotes: 4

Related Questions