Platform: router-store: need an actual navigation action

Created on 23 Aug 2017  路  8Comments  路  Source: ngrx/platform

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[x] Feature request
[ ] Documentation issue or request
[ ] Support request

What is the current behavior?


The router-store doc states that

During the navigation, before any guards or resolvers run, the router will dispatch a ROUTER_NAVIGATION action.

And that

The ROUTER_CANCEL action represents a guard canceling navigation.
A ROUTER_ERROR action represents a navigation error.

How is it possible to know if the route has really been navigated to?
Something other than the initial intent (ROUTER_NAVIGATION) and which assures that the navigation hasn't been canceled (ROUTER_CANCEL/ROUTER_ERROR).

Expected behavior:

Need a new action like ROUTER_ACTIVE in order to be sure that the navigation has been actually done and passed the guards checks, etc.

Minimal reproduction of the problem with instructions:

Version of affected browser(s),operating system(s), npm, node and ngrx:

Other information:

Most helpful comment

@fsenart

Meanwhile I leave here the code I'm using that emmits a "ROUTER_ACTIVE" as you suggested. Maybe it is useful to you or someone encountering this thread.

import { Injectable } from '@angular/core';
import { Router, NavigationEnd, RoutesRecognized, RouterStateSnapshot } from '@angular/router';
import { RouterNavigationAction, RouterStateSerializer, RouterReducerState } from '@ngrx/router-store';
import { Store } from '@ngrx/store';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/withLatestFrom';

export const ROUTER_ACTIVE = 'ROUTER_ACTIVE';

@Injectable()
export class RouterStoreExtension
{
    constructor(
        router: Router,
        stateSerializer: RouterStateSerializer<RouterStateSnapshot>,
        store: Store<{ routerReducer: RouterReducerState }>,
    )
    {
        router.events.subscribe(e =>
        {
            if (e instanceof RoutesRecognized)
                this.lastRoutesRecognized = e;
        });

        router.events
            .filter<NavigationEnd>((event) => event instanceof NavigationEnd)
            .withLatestFrom(store.select<RouterReducerState>(state => state.routerReducer))
            .subscribe(([event, routerReducer]) =>
            {
                // when timetraveling the event.id is an old one, but the navigation triggered uses a new id
                // this prevents the dispatch of ROUTER_ACTIVE when the state was changed by dev tools
                if (routerReducer.navigationId === event.id)
                {
                    const routerStateSer = stateSerializer.serialize(router.routerState.snapshot);
                    store.dispatch({
                        type: ROUTER_ACTIVE as any,
                        payload: {
                            routerState: routerStateSer,
                            event: new RoutesRecognized(
                                this.lastRoutesRecognized.id,
                                this.lastRoutesRecognized.url,
                                this.lastRoutesRecognized.urlAfterRedirects,
                                routerStateSer as any
                            ),
                        }
                    } as RouterNavigationAction<RouterStateSnapshot>);
                }
            });
    }

    protected lastRoutesRecognized: RoutesRecognized = null;
}

All 8 comments

You can use the Router's events Observable for this and listen for the NavigationEnd event

Hi @brandonroberts,

Thank you for your answer. However, noticing that I'm new to the world of ngrx, I have two questions/remarks:

  1. I think there is a lack of documentation around the fact that any observable can be used as the source of effects. I mean I was literally brainwashed by the current doc and thought that the only way to play with effects is to begin with the actions observable. It may be interesting to underline somewhere in the doc that any observable can do the job.
  2. Can you please explain what is the advantages of router-store over using router's events for even NavigationStart, Cancel, Error, etc. What is the underlying philosophy behind router-store which makes having NavigationStart, Cancel, Error in it natural but excludes NavigationEnd?

@fsenart

  1. You make a good point here. Its more implicit that Effects provide additional sources of actions regardless of their source stream. That is something we can improved in the documentation definitely.

  2. The biggest advantage is state reduction as part of the navigation process. Router-store uses an optimistic update for the state reduction, so you get the ability to roll back the router state snapshot in case of navigation being cancelled or erroring. The NavigationEnd event doesn't provide a router state snapshot since the router has finished rendering.

Hi all,

I鈥檓 finding the need for a ROUTER_ACTIVE action too.

Because at the time of ROUTER_NAVIGATION action is issued (and grabbed in an effects for example), the active route is not yet available in the Router injectable service. This is problematic if not all info was serializable into the action payload, or more importantly, if there is the need to interact with the Router giving it the current activated route, for example, when calling router.navigate().

I鈥檝e found that using the NavigationEnd events approach (instead of an action) to drive effects is not compatible with time-travel. The router-store calls router.navigateByUrl() on changes of the routerReducer state, making the NavigationEnd trigger and the effects run again, in some cases entering a loop.

I think I will fork the repo and add the ROUTER_ACTIVE action.
This way the business logic is uniform dealing always with actions and not router events and actions.

If someone have a better approach, please indicate so.

@fsenart

Meanwhile I leave here the code I'm using that emmits a "ROUTER_ACTIVE" as you suggested. Maybe it is useful to you or someone encountering this thread.

import { Injectable } from '@angular/core';
import { Router, NavigationEnd, RoutesRecognized, RouterStateSnapshot } from '@angular/router';
import { RouterNavigationAction, RouterStateSerializer, RouterReducerState } from '@ngrx/router-store';
import { Store } from '@ngrx/store';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/withLatestFrom';

export const ROUTER_ACTIVE = 'ROUTER_ACTIVE';

@Injectable()
export class RouterStoreExtension
{
    constructor(
        router: Router,
        stateSerializer: RouterStateSerializer<RouterStateSnapshot>,
        store: Store<{ routerReducer: RouterReducerState }>,
    )
    {
        router.events.subscribe(e =>
        {
            if (e instanceof RoutesRecognized)
                this.lastRoutesRecognized = e;
        });

        router.events
            .filter<NavigationEnd>((event) => event instanceof NavigationEnd)
            .withLatestFrom(store.select<RouterReducerState>(state => state.routerReducer))
            .subscribe(([event, routerReducer]) =>
            {
                // when timetraveling the event.id is an old one, but the navigation triggered uses a new id
                // this prevents the dispatch of ROUTER_ACTIVE when the state was changed by dev tools
                if (routerReducer.navigationId === event.id)
                {
                    const routerStateSer = stateSerializer.serialize(router.routerState.snapshot);
                    store.dispatch({
                        type: ROUTER_ACTIVE as any,
                        payload: {
                            routerState: routerStateSer,
                            event: new RoutesRecognized(
                                this.lastRoutesRecognized.id,
                                this.lastRoutesRecognized.url,
                                this.lastRoutesRecognized.urlAfterRedirects,
                                routerStateSer as any
                            ),
                        }
                    } as RouterNavigationAction<RouterStateSnapshot>);
                }
            });
    }

    protected lastRoutesRecognized: RoutesRecognized = null;
}

it would be super useful to expose the activatedRouteSnapshot at time of NAVIGATION_ACTION
I find that i have to "drill" down the router state children and find the last child to get any resolved data, is there a better way?
@akaztp thanks for your code! should be a PR

a bit related https://stackoverflow.com/a/46379941/301596 the problem I had was, apart from getting the whole state tree and having to "drill" down to get the last activated child (and in my case I wanted to get params and queryParams rather than any resolved data, so I could use ROUTER_NAVIGATION), that the last child wasn't always the one I needed information from, consider this case /articles/:id/graph / /articles/:id/table, say you wanted to save the :id to the store, the last child wouldn't have the information..

@nadavsinai @fxck
I've encountered the same problem in the last days, so I've produced a solution which I have just published.
I've found that is in the same line of the stackoverflow answer @fxck mentioned.

The ideia is to identify the configured routes that need to be referred later. This allows for a light serialization of the router state in ROUTER_NAVIGATION action. Which in turn allows for time-travel to work on ReduxDevTools. See more on the repo. Hope it is useful to you.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

smorandi picture smorandi  路  3Comments

hccampos picture hccampos  路  3Comments

brandonroberts picture brandonroberts  路  3Comments

NathanWalker picture NathanWalker  路  3Comments

mappedinn picture mappedinn  路  3Comments