Search code examples
angularngrxangular-routerngrx-effects

ngrx dispatching action on component (associated with a route) init


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?


Solution

  • 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).