NgRx Tips — Part 4
It’s been a while since I’ve written one of these (only a full year), but recently I’ve been doing some front-end work and have more tips to share! Exciting!
Application Specific Store Type
When you inject the store, you’ll likely have these two imports:
import { Store } from '@ngrx/store';
import { ApplicationState } from 'app/app.state';...constructor(private store: Store<ApplicationState>) {
Since there’s only 1 interface for the state, it seems kind of excessive to need to import both every single time, doesn’t it? We can fix that!
export class MyAppStore extends Store<ApplicationState> {}#app.module
providers: [
{
provide: MyAppStore,
useExisting: Store
}
]#component
import { MyAppStore } from 'app/app.state';...constructor(private store: MyAppStore) {
As an added bonus, if NgRx ever bites the dust, you’ll have a layer of abstraction between the new next-best-thing and your code (assuming it follows a similar observable pattern)
TypeScript strict “Object is possibly ‘null’”
In my latest project I decided to hop aboard the pain train and turn the full strict configuration on, and I ran into this scenario quite a bit:
return store.pipe(
select('feature', 'item'),
filter(i => i != null),
map(i => i.toString()) // [ts] Object is possibly 'null'
);
Although we know what we meant, filter
rarely helps us out type-wise (note the instanceOf
method from an earlier article). To resolve this issue I created a new pipe-able operator:
export function hasValue<T>(): OperatorFunction<T | null | undefined, T> {
return (source: Observable<T | null | undefined>): Observable<T> => {
return source.lift(new FilterOperator(val => val !== undefined && val !== null));
};
}return store.pipe(
select('feature', 'item'),
hasValue(),
map(i => i.toString()) // hurray!
);
Routing
If you’re using NgRx to maintain the state of your application, avoid using the router to store anything: no resolves, no route subscriptions, etc. Instead, as routes are hit, use guards and canActivate
to keep the NgRx state in sync with the current route. To do so, I usually follow this pattern:
- Check the current state: are we already in sync (user left and hit the back button maybe) with this route? If not, dispatch an action (that may trigger an
effect
) to get things in sync. - Return
true
(orfalse
if a decision is being made) once the state is back in sync.
Here’s an example around authentication:
@Injectable()
export class AuthenticationGuard implements CanActivate {
constructor(private store: MyAppStore) {}canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.store.pipe(
select('core'),
take(1),
switchMap((c: CoreState) => {
if (!c.authenticated && !c.authenticating) {
this.store.dispatch(new AuthenticateAction());
}return this.store.pipe(
select('core'),
filter(s => !!s && !s.authenticating),
map(s => s.authenticated)
);
})
);
}
}
Here’s an example where we’re just waiting for data to load:
@Injectable()
export class MyItemGuard implements CanActivate {
constructor(private store: MyAppStore) {}canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const myItemId = route.params['MyItemId'] as string;return this.store.pipe(
select('items', 'itemId'),
take(1),
switchMap(id => {
if (id !== myItemId) {
this.store.dispatch(new MyItemLoadAction(myItemId));
}return this.store.pipe(
select('items', 'item'),
hasValue(),
map(() => true)
);
})
);
}
}
I think that’s about it for now: short and sweet! Going forward I’ll try and keep better track of these tips to make sure I’m not missing anything.