Search code examples
angularangular-materialngrx

How to update a mat table row when NgRx is used?


I have a requirement to display some data using a mat table. One of the columns will contain a slide toggle through which I can change a property (let's call it 'active').

I'm also using NgRx to store/modify the data.

So, the state is global and saved in the NgRx store. My solution is to get the data using a selector and define an action for changing the 'active' property. The action will be handled by a reducer.

After the data is changed, the selector will return a new value and the table will reflect that.

This is the main component:

ngOnInit() {
   this.dataSource = this.store.pipe(select(selectPeriodicElements));
}

updateActiveStatus(element: PeriodicElement) {
   this.store.dispatch(toggleActiveStatus({id: element.id}));
}

Here is the complete example

My issue is that the solution doesn't seem to be efficient. If I use the developer tools to inspect the DOM and I update one row, then every row seems to be redrawn, not just the one which was updated. Also, the animation for the slide toggle is missing (because the entire table is being redrawn I guess).

I searched for a better alternative, but I wasn't able to find one, even though this should be a popular requirement.

So, my question is how to update a row from a mat table when NgRx is being used?

Thank you!


Solution

  • The rows are rerendered when their data has been changed. Therefore I would blame reducer of toggleActiveStatus.

    In the complete example we can find JSON.parse(JSON.stringify(state.elements)), that's the worst case scenario for ngrx store :) because it means everything in the state has been changed and as result - everything is rerendered.

    Instead of JSON always update the node you want to change, nothing else. This rule will help you to deliver well performed apps.

    The code below implements this behavior.

    const periodicElementsReducer = createReducer(
      initialState,
      on(toggleActiveStatus, (state, {id}) => {
        return {
          ...state,
          elements: state.elements.map(element => element.id !== id ? element : {
            ...element,
            activate: !element.activate,
          }),
        };
      })
    );
    

    The issue with animation comes from the table update. We click the toggle, it starts animation, the table gets new data and rerenders the toggle with its new state, that kills the animation. Therefore we need to delay the store update, the best part would be to do it in dispatch for example.

    The fix can be like that

      updateActiveStatus(element: PeriodicElement) {
        setTimeout(() => this.store.dispatch(toggleActiveStatus({id: element.id})), 150);
      }