NGRX Common Gotcha’s

Mark Sargent
4 min readJun 13, 2018

--

I have used NGRX on a few projects, and have often joined teams adopting the framework with developers learning it from scratch. There are a set of queries and issues that often come up as people learn NGRX, and in this series I will discuss the most common ones.

My previous article discussed the types of state that you might place into the store. This article covers common gotcha’s I have noticed when reviewing merge requests within teams adopting NGRX.

Not Unsubscribing

The most common issue I see with developers new to NGRX (or more specifically in this case, RXJS), is introduction of potential memory leaks into components.

Angular will automatically unsubscribe any observables used in the template with an async pipe,. Therefore as long as there are no other subscriptions then this is the simplest case and no other code is necessary. In ngOnInit:

this.customer$ = this.store.select(selectors.selectCustomer)

Then in the template:

<div *ngIf = "customer$ | async as customer">
… etc …
</div>

However if you subscribe to the observable in your component, you introduce a potential memory leak:

customerIsCool = false;ngOnInit() {
this.store.select(selectors.selectCustomer)
.subscribe((customer) = > {
this.customerIsCool = customer.supports === 'Southend';
});
});

There are various options for fixing this issue. If you know you will only be interested in the value once, use take(1) to unsubscribe as soon as you get the value:

ngOnInit() {
this.store.select(selectors.selectCustomer)
.pipe(take(1))
.subscribe((customer) = > {
this.customerIsCool = customer.supports === 'Southend';
});
});

If the value could change during the lifetime of the component, you need to unsubscribe when the component is destroyed. An elegant solution to this is to declare a Subjectproperty:

private ngUnsubscribe: Subject<void> = new Subject<void>();

Implement onDestroy and complete the observable in ngOnDestroy:

ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}

Then in your subscriptions, takeUntil ngUnsubscribe emits a value:

ngOnInit() {
this.store.select(selectors.selectCustomer)
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((customer) = > {
this.customerIsCool = customer.supports === 'Southend';
});
});

To conclude: the default choice should be to use the async pipe, if you need to subscribe and the value will not change then user take(1), otherwise use the ngUnsubscribe approach.

Not using feature selectors

You can select state from the store in a component like this:

this.customers$ = this.store.select('customers')

I would however always recommend use of feature selectors to get rid of the magic string (which makes refactoring difficult i.e. might break if the store is restructured) and take advantage of memoization.

See the selectors documentation for details re; feature selectors. For example in customer.selectors.ts:

import { MemoizedSelector, createFeatureSelector, createSelector } from '@ngrx/store';import { CustomerState} from './customer.reducer';export const selectCustomersState: MemoizedSelector<object, CustomerState> = createFeatureSelector<CustomerState>('customers');

Then in the component:

Import * as selectors from '../../state/customer/customer.selectors.ts';ngOnInit() {
this.customers$
= this.store.select(customerSelectors.selectCustomers);
}

The selector acts as a facade to the structure of the store, so if the state structure is ever refactored only the selector will have to change and the components can remain the same.

Using appropriate flattening operators in effects

This issue is explained very well in articles RxJS: Avoiding switch Map-Related Bugs, Your NGRX Effects are Probably Wrong and in a very entertaining way here so I won’t go into too much detail in this article.

switchMap is often used as the default choice in effects, however developers have to bear in mind that this cancels any prior observables. So if the effect is kicking off an API call, results of pending API calls will be ignored.

This is perfect for some use cases (in particular searching) where you are only ever interested in the result of the latest API call. However for updates you will often need to process the result of each API call, in which case mergeMap or concatMap are better options.

Avoid unecessary API calls

If a component uses immutable data from an API call, I have often seen the data being loaded every time the component is rendered. Immutable data only needs to be loaded once, so we can reduce the number of API calls being performed.

Assuming you are using a “loading” boolean (which is currently the default generated pattern by angular-cli or ngx-reduxor), then you can handle this in the effect that kicks off the load. Use withLatestFrom to get the latest state in the effect, and select the state being loaded and the loading (using a feature selectors!). Only kick off the API call if there is no state and loading is false.

@Effect() loadSomeData$ Observable;constructor(private actions$: Actions, private store$: Store) {this.loadSomeData$: Observable = this.actions$
.ofType(actions.LOAD_DATA)
.withLatestFrom(this.store$)
.switchMap(([action, state]) => {
const currentState = selectors.selectSomeState(state);
const loading = selectors.selectLoading(state);
if (currentState || loading) {
// We either already have the state in the store,
// or are already loading, so there is no need to
// kick off another API call
return empty();
}
return doApiCall()
.map((response: LoadDataResponse) => {
return new actions.Success(response);
})
.catch(() => of(new actions.Failed()));
});

Ensure none of the state tree is mutated

I have seen many examples of developers taking copies of top level state objects, but copying references of nested objects. Reducers should always return new state objects, instead of mutating the previous state, and this applies at any level in an object tree.

The simple fix to ensure this does not happen is to use the NGRX store freeze meta reducer (in development and tests), which will throw an exception if state is mutated.

The redux documentation provides guidance here for ensuring that reducers perform basic update operations immutably.

--

--

Mark Sargent
Mark Sargent

Written by Mark Sargent

Full Stack Developer (Angular/.Net) using agile methodologies to deliver quality, maintainable software. Recent focus on use of NGRX in enterprise applications.

Responses (3)