angular 5+, ngrx 4+
What I'm trying to do is trigger ARTICLES_LIST_REQUEST
when ArticlesListComponent
, associated with path
/articles
is initialised.
There's a plethora of ways to do it, using dispatch
inside ngOnInit
, using dispatch
on a guard for example, but what I want to do instead is to use @ngrx/router-store
, listen to ROUTER_NAVIGATION
and map it to ARTICLES_LIST_REQUEST
(actually ARTICLES_LIST_PARAMS_SET
, to save current route params to the store, but it doesn't matter), now there's a couple of problems with that.
Since effects, even those registered with forFeature
, are running globally, if I were to set up this effect listening to ROUTER_NAVIGATION
inside multiple module (articles, people, brands etc.), it would run multiple times, triggering all the actions at once, so I'd need a way to filter it only for the route I'm interested in.. but how? Since there are no named routes in angular router and navigation action returns a snapshot of the whole tree, there's no way to know that the particular route just activated. What I'm doing at the moment is checking and filtering by the url, for example whether it begins with /articles
and whether the next character is not /
(to filter out /articles/<id>
. But I'm really disliking it, doesn't really feel like it will safely cover all the cases.
There used to be a way to pause / start / unsubscribe / subscribe effects inside a component, which I don't think is really possible anymore, but it would be moving the responsibility back to component, which wouldn't be much better than dispatching the action from component / guard.
Should I not be using router-store
for this at all?
Here's what I ended up with:
1) Add a unique name
to route definitions's data
export const ROUTES: Routes = [
{
path: '/articles',
component: ArticlesListPage,
data: {
name: 'articles-list'
}
}
];
2) create a custom serializer for router-store
that extracts only params
, queryParams
and a name
(from data.name
) for each child in tree
function createSerializedTree(node: ActivatedRouteSnapshot) {
const tree = {
queryParams: node.queryParams,
params: node.params,
name: node.data.name,
children: node.children.map(createSerializedTree),
firstChild: undefined
};
tree.firstChild = tree.children[0];
return tree;
}
3) create a function for let
that filters and maps to the first route with name matching the one you provide
const _checkIsNodeHasName = (name: string, node: SerializedRouterNode) => {
if (node.name === name) {
return node;
} else if (node.firstChild) {
return _checkIsNodeHasName(name, node.firstChild);
} else {
return undefined;
}
}
export const filterAndMapRoute = (name: string) => {
return (navigationAction$: Observable<RouterNavigationAction<RouterStateUrl>>) => {
return navigationAction$
.map(action => action.payload.routerState)
.filter((state) => !!_checkIsNodeHasName(name, state.root))
.map((state) => _checkIsNodeHasName(name, state.root));
};
};
Now I can do this
@Injectable()
export class ArticlesEffects {
@Effect()
private _onRouteChange$ = this._action$
.ofType(ROUTER_NAVIGATION)
.let(filterAndMapRoute('posts-list'))
.map(/* ... */) // map to whatever action I need
}
It's more or less just a proof of concept, realistically I'd need to check not only the firstChild but all of children.
I first went with simply the last firstChild
of the tree, but there are cases where that wouldn't work, for example if you wanted to save id
from these routes
/articles/:id/graph
/ /articles/:id/table
, the last firstChild
, wouldn't contain the id
, thus the need to find the first child that matches the name and map to that (and filter if none was found).