In this article, I will tell you how to avoid simple pitfalls while working with ngRx to improve your family-work balance:-)
Introduction
If you work with Angular you definitely know about the most popular state management system for Angular applications ngRx/Store.
Let's recall what it is to be on the same page:
@ngrx/store
… is RxJS powered state management for Angular applications, inspired by Redux. Store is a controlled state container designed to help write performant, consistent applications on top of Angular.
Here is the flow diagram from official documentation describing how it works:
Components send Actions that is an object with a mandatory property type and optionally other properties with data to be stored to the central object — a Store. Actions are being handled by reducers , a special functions that make data from actions and put them to Store (or modify Store). Other components can subscribe to Store updates (to a specific part of the Store) with selectors (actually selectors determines which part of the Store updates you want to monitor). In simple cases, selectors receive state object as an argument and return you some property of the state object:
(state) => state.prop1
Before I start to keep your time let's review the simplest example. Not to re-invent the bicycle I will use the simplified example from the official doc.
We will create a Store with counter value, and action/reducer that increases that counter value.
Let's create an action:
// src/app/counter.actions.ts
import { createAction } from '@ngrx/store';
export const increment = createAction('[Counter Component] Increment');
Reducer:
//src/app/counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment } from './counter.actions';
export const initialState = 0;
const _counterReducer = createReducer(initialState,
on(increment, state => state + 1)
);
export function counterReducer(state, action) {
return _counterReducer(state, action);
}
Add StoreModule module to app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({ count: counterReducer })
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
You can see that we specify our reducer in this line
StoreModule.forRoot({ count: counterReducer })
To read count value we just need to use select function in a component:
// in some component
import { Store, select } from '@ngrx/store'
...
constructor(private store: Store<{ count: number }>) {
this.count$ = store.pipe(select('count'));
// now we have observable that will emit values on each count update
// old school approach
//this.count$ = store.pipe( select(state => state.count));
}
What if we don't want to keep count in the main app module?
We can put it to a feature module.
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
StoreModule.forRoot({}),
StoreModule.forFeature('featureName', { count: counterReducer })
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Now our selector to grab the value from feature-branch of the Store state will be:
// count.selectors.ts
export const selectFeature = createFeatureSelector<FeatureState>('featureName');
export const countSelector = createSelector(selectFeature, (state) => state.count);
// And in some component
this.count$ = store.pipe( **select** (countSelector));
Now let's check how all this works:
All our actions and Store state changes we can observe with nice Chrome plugin: Redux DevTools:
- Install a plugin in Chome
- install @ngrx/store-devtools module to your Angular app: — ng add @ngrx/store-devtools
Or npm i @ngrx/store-devtools (in that case you should add StoreDevtoolsModule to AppModule manually)
monitor your Store in Chrome Dev Tools (Redux tab)
Simple, right?
At that place, you may ask yourself why do we need an article that just represents the official documentation example? Because even with these simple flows you can spend hours on debugging if something doesn't work as expected.
I revealed 5 often mistakes in my (and my fellow developers) practice.
#1. Redux-DevTools doesn't display undefined props in actions
Say we have an action that sends not the only type of message but also some additional info:
{
type: SOME_TYPE,
value: this.someProp
}
For that purpose lets modify a bit our code:
// counter.actions.ts
...
export const increment = createAction('[Counter Component] Increment', props<{value: number}>());
// counter.reducer.ts
const counterReducerFunc = createReducer(initialState,
on(increment, (state, {value}) => state + 1 + value)
);
//app.component.ts
public value;
...
increment() {
// provide additional value to actionCreator function this.store.dispatch(increment({value: this.value}));
}
Now our reducer should increase state value by 1 and add value.
But, something goes wrong and you want to debug the actions in Redux Dev Tools.
Ok, count got NAN value, this is not correct. And why don't we see value prop in action tab content in ReduxDevTools? Only type field is present.
The answer is that a) we forgot to assign some number to value property, b) Chrome plugins cannot get undefined values since it cannot be stringified.
Let's assign value with 0 .
//app.component.ts
public value = 0; // or other number value
Now we can observe this prop in ReduxDevTools:
I spend an hour to reveal it. Maybe you will waste less time after reading this:)
You can take a look at code in this branch of the article GitHub repo. Or check it in a ng-run.com playground.
Take away: better to use null if you need to specify empty value since null can be stringified and can be shown in ReduxDevTools.
#2. StoreDevModule may slow down the app
Once upon a time, I had a big list of objects in a Store. And some user operations modified data on the specific Action type and put them back to Store and then components displayed that data.
What our testers observed that starting from a few hundreds of items in a list each user operation caused small but noticeable UI update lags. And this was not rendering but JS issue.
After checking with ChromeDevTools on Performance tab (you can read more about it here) I got this picture:
Do you remember why it may happen? Yes, because we send data to our ReduxDevTools plugin to be able to monitor Store Actions and state.
Since we added StoreDevToolsModule manually to the AppModule — we missed an option to turn it off for prod builds:
imports: [
StoreModule.forRoot({}),
StoreModule.forFeature(featureKey, {items: itemsReducer}),
BrowserModule,
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }), // missed logOnly option
EffectsModule.forRoot([AppEffects])
],
After I added it — UI started to feel much better:-)
Takeaway: do not forget logOnly option when you use StoreDevtoolsModule to monitor your ngrx/Store activities. Actually, if you install it with ng add @ngrx/store-devtools then this option will be added automatically. You can read more about logOnly here.
You can play with the code in GitHub repo branch. Or start this branch on ng-run.com Angular playground by Alexey Zuev. For that just copy GitHub branch link and add ng-run.com/github/ like this:
Branch link:
https://github.com/kievsash/ngrx-store-and5sillyMistakes/tree/pitfall_2_StoreDevtoolsModule_slow_down
Now let's start it on ng-run.com by this link (copy it to browser address bar):
https://ng-run.com/github/kievsash/ngrx-store-and5sillyMistakes/tree/pitfall_2_StoreDevtoolsModule_slow_down
#3. You import feature module but it doesn't work
a) Ok, So you have nice feature Angular module where you put:
// feature.module.ts
...
imports: [
StoreModule.forFeature(featureKey, {items: itemsReducer}),
...
You expect it should work when you added it to app.module.ts AppModule imports. But… it doesn't) You open a ChromeDevTools console and see:
Ok, so we go to ngrx/platform GitHub repo and search for 'ReducerManager' entity. And see that it is provided as REDUCER_MANAGER_PROVIDERS by StoreModule.forRoot(…) call here.
Answer is obvisous: we forgot to include StoreModule.forRoot({}) in out AppModule.
// app.module.ts
imports: [
StoreModule.forRoot({}),
StoreModule.forFeature(featureKey, {items: itemsReducer}),
Now it works well.
b) I found one more interesting behavior but with StoreDevtoolsModule
Ok, so you added it to AppModule:
imports: [
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }),
StoreModule.forRoot({}),
StoreModule.forFeature(featureKey, {items: itemsReducer}),
But when you open Redux tab in ChromeDevTools you see this:
Why??
Because I just put StoreDevtoolsModule in imports array BEFORE StoreModule.forRoot. So it seems like Angular tries to instantiate it before any Store is created. Just put StoreDevtoolsModule AFTER StoreModule.forRoot in AppModule decorator imports array to fix the issue.
imports: [
StoreModule.forFeature(featureKey, {items: itemsReducer}),
StoreModule.forRoot({}),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }),
Now it works good:
Interesting that in Angular 9 putting StoreModule.forFeature BEFORE StoreModule.forRoot doesn't create any issue.
You can find the code to play with here.
#4. Exported reducer function is necessary as function calls are not supported by the AOT compiler (in Angular 8).
The title of this pitfall sounds not clear but actually it is very simple. You have reducer:
export const counterReducer = createReducer(initialState,
on(increment, state => state + 1),
on(decrement, state => state - 1),
on(reset, state => 0),
);
@NgModule({
declarations: [],
imports: [
StoreModule.forRoot({ count: counterReducer })
],
providers: [],
})
export class CounterStateModule { }
And it works quite well…until we try to build the production code:
ERROR in Error during template compile of 'CounterStateModule'
Function calls are not supported in decorators but 'createReducer' was called in 'counterReducer'
'counterReducer' calls 'createReducer' at app/counter.state.ts
This is a well-know issue, you can read more about it here.
Fortunately, when I tested it on Angular 9 project with Ivy (ngRx/Store 8.6) — it was already solved! You can read more details in this issue.
You can check the code here.
Takeaway: update to Angular 9 😎
#5. Action creator is a function but if you forget to put parentheses — ngRx keeps silence.
Here is a possible pitfall reproduce code:
constructor(private store: Store<{ count: number }>) {
}
selectAll() {
this.store.dispatch(select);//should be select() but no type error
}
unselectAll() {
this.store.dispatch(unselect()); // correct
}
Typescript will not help you here. But fortunately, you will find a hint in ChromeDevTools console:
Takeaway: Do not put all eggs in typescript basket 🎓 Sometimes it may not help you.
Conclusion
Ok, so what we have learned in this article?
- Use null instead of undefined as noValue to be able to observe it in ReduxDevTools Chrome plugin.
- StoreDevModule may slow down the app. To prevent it — set logOnly option as true.
- Do not forget to put StoreModule.forRoot({}) in AppModule when you connect other ngrx/Store feature modules.
- Exported reducer function is not necessary in Angular 9.
- Do not forget to put parentheses in your call action creator function.
Now you can spend more time with your family but not in debugging sessions⛷
Let’s keep in touch on Twitter!
BTW. I started a video tutorial series “Angular can waste your time” on Youtube. There I will be publishing videos about resolving tricky cases with Angular and RxJS. Take a look!
Cheers!