24/08/2020

Contributing to the NgRx Ecosystem

Written by Stephen Cooper, a Developer at G-Research

What is the great thing about open source software? Not only do you get to use amazing tools but you also have the opportunity to contribute improvements back into the original software. In my team at G-Research we use Angular, along with NgRx, to write our web applications. After some unexpected behaviour in our app I found a way to leverage NgRx to pin point future bugs. What follows is an account of how this check developed and was later accepted into the NgRx codebase along with a new linting rule for ngrx-tslint-rules.

An Unexpected Error

The performance of one of our Angular applications was starting to degraded. I tracked down the issue to a setInterval that repeatedly caused our Angular application to re-render. This can be an expensive operation and it is widely accepted that you should keep Angular change detection to a minimum. After re-writing the code to avoid the need for a setInterval the performance of the page improved greatly. However, after the release, we started to see odd behaviour in another part of the application.

The expected work flow was:

  1. User selects a date from a dropdown.
  2. On closing the dropdown the selection is emitted.
  3. An api call is triggered returning the data.
  4. A chart updates to display the new data.

However, after removing the setInterval, steps 1-3 completed but the chart would not update until the user interacted with the page. I could easily confirm that the data was arriving at the chart component by adding logging to the observable pipe.

chartData$ = this.data.pipe(
    tap(data => console.log("Data updated", data)
);

So why wasn’t the chart updating? And why does the data ‘suddenly’ appear when the user interacts with the page?

Whenever you run into a situation like this you likely have a change detection issue. In this case, Angular is not running change detection after the data was updated. But why?

NgZone and Change Detection

If you are not familiar with Zones in Angular this article goes into more detail. In summary asynchronous tasks can either run inside or outside of Angular’s change detection zone. The delayed updating of the chart suggests the update event is running outside of Angular’s zone.

It’s All About the Source Event

It took me a while to understand that I should be focusing on the source event, the selector in step 1, and not the end of the event chain when the chart data is updated. Any event started outside of Angular’s zone will run to completion without ever triggering change detection. No change detection, means no update to our chart. This is sometimes desired for performance but we won’t go into that here.

If we trace our chart update back through the api call, the NgRx Effect, the dropdown output event and finally to the eventEmitter inside the dropdown component I discovered the following code.

@Component({...})
export class DropdownComponent implements OnInit {

    @Output()
    updateSelection = new EventEmitter<any>();

    ngOnInit(){
        $('#dropdown').on('hidden.bs.dropdown', () => {
            this.updateSelection.emit(this.selections);
        });
    }
}

jQuery Event Handler

This code uses jQuery to watch for the hidden event of a Bootstrap dropdown. This enables the component to fire an event when the dropdown is closed. The critical thing is that the hidden.bs.dropdown event is fired outside of Angular’s zone meaning this entire chain of events will not trigger change detection. Even though the chart data is updated the chart is not re-rendered until change detection is run. In our application change detection was being triggered regularly by the setInterval which is why we did not notice this issue before.

Solving with NgZone

To fix this issue we need to make Angular aware of this event. We do this by wrapping the EventEmitter with ngZone.run() method as follows.

$("#dropdown").on("hidden.bs.dropdown", () => {
  this.ngZone.run(() => {
    // Bring event back inside Angular's zone
    this.updateSelection.emit(this.selections);
  });
});

Another solution would be to use an Angular specific component library like ng-bootstrap which takes care of firing events within ngZone for you.

This means the event is now tracked by Angular and when it completes change detection will be run! As we have applied this fix within our DropdownComponent all subsequent events forked off this originating one will also be checked. Important when using NgRx Actions and Effects!

Improve Tooling to Catch Earlier

Debugging this issue took a significant amount of time and revealed that this type of bug could result from a seemingly unrelated code change. Wouldn’t it be great if we could write a check that would immediately highlight events running outside of ngZone during development?

As we are using NgRx and changes are triggered by dispatching Actions to the store we have a central place to apply our check. We can do this with a meta reducer that checks that every dispatched Action is running in Angular’s zone. Reverting my fix to the dropdown component and adding in the meta-reducer immediately pin pointed the issue.

//NgRx Meta Reducer to assert always in NgZone
export function assertInNgZoneMetaReducer(reducer) {
  return function (state: State, action: Action): State {
    NgZone.assertInAngularZone();
    return reducer(state, action);
  };
}

Share with Community

After having great success with this meta reducer I placed it in an internal G-Research library to enable us to apply the same check in other applications. It caught a long standing issue in another application! As a result of this I shared it on Twitter and turns out the authors of NgRX thought this was a good idea and invited me to submit a PR to add it.

I raised this issue in the NgRx repo and was asked to create the PR to make the change.

Adding the strictActionWithinNgZone Check

Creating the check was fairly straightforward as the existing NgRx runtime checks provided a clear pattern to follow. I was impressed with the simplicity and clarity of the code and the extensive tests. I learnt a lot from the code base which was an unexpected benefit of making the change myself.

While adding the new check I updated the documentation for the existing runtime checks and improved the error messages to help users quickly understand the issue.

It all resulted in the following PR.

Cleaner and Easier to Share Within G-Research

The check was released in v9 of NgRx. This meant we could delete the code from our internal library and simply enable the check with a flag as opposed to the more complicated setup to share an external meta-reducer.

Here is my post announcing the new feature and how to take advantage of it.

NgRx Linting

On a seperate occasion I found a bug in one of our NgRx reducers. I was surprised that Typescript did not catch the typo but turns out this is currently expected. There is an open issue to get the types to flow through but this has not been solved yet. In the meantime the recommendation is to explicitly type your reducer functions to ensure Typescript can catch errors.

But how do you enforce this across a code base with many developers? It is easy to forget the return type and let bugs creep in again. This is where we can take advantage of linting rules. Turns out this was not as hard as I thought, once you understand Abstract Syntax Trees. This resulted in me creating a PR against the ngrx-tslint-rules library.

As we were already using this library it was then just a matter of updating our version to enable this check across our apps. After applying the rule and adding in all the explicit return types to our code a number of potential bugs were corrected due to missed typos.

Another reason why it is great to push changes back into the open source libraries is that you then get on-going support and fixes for the checks. For example there was a slight bug in my original rule that has already been fixed by another developer.

Conclusion

Why contribute back to OSS? You learn from working in high quality code bases. Reduction of your own tech debt as code will remain compatible with future releases. On-going support and fixes from others in the community. It’s a win win situation for everyone!

Finally it is not as hard as you might think to contribute to these libraries and in my experience the authors are very supportive. I am very grateful that G-Research is moving more and more into OSS enabling us as developers to contribute back to the software we use every day.

Related articles

Stay up to-date with G-Research

Subscribe to our newsletter to receive news & updates

You can click here to read our privacy policy. You can unsubscribe at anytime.