Angular: Refetch data on same URL navigation

Different approaches with their pros and cons

Real time applications normally use WebSockets or some server push technology. In case the backend doesn’t support a push technology we often use some sort of polling mechanism to get the latest data.

However there are use cases where we want to give control to the user and let him decide when the data should be refreshed. For example by clicking on a refresh button or a route navigation item while that particular route is already displayed.

By default the router doesn’t emit router events when we try to navigate to the same URL that is already activated. Which generally is a good thing.

But how can we refetch data on route refresh? 🤔

There are different strategies to solve this problem. Each of them has some advantages and some downsides. Let’s have a look at them.

1. Emit route events on navigation to currently active route

The first approach is to tell the Angular router to emit the route events on refresh and then handle them accordingly in our routed component.

In early Angular versions there was no option to to tell the router to emit events on same route refresh. Angular 5.1 introduced the onSameUrlNavigation property on the routers ExtraOptions.

The onSameUrlNavigation property defines what the router should do if it receives a navigation request to the current URL. By default, the router will ignore this navigation. However, this prevents features such as a "refresh" button. Use this option to configure the behavior when navigating to the current URL. Default is 'ignore'.

The onSameUrlNavigation property accepts either 'reload' or 'ignore' as values. We need to set this property to reload.

Inside our ExampleRouteComponent we then subscribe to the router events to get notified about the route changes. Notice that the router fires a lot of events while routing. We are only interested in the NavigationEnd and therefore filter out all other RouterEvents.

We subscribe to our filtered router event stream and fetch the data.

The NavigationEnd event gets fired every time we route to a component. Doesn’t matter if it is our ExampleRouteComponent or another one.

Even though our component gets destroyed when we navigate away the current implementation would refetch the data on every navigation. Any ideas why?

Follow me on Twitter or medium to get notified about my newest blog posts!🐥

Although the component gets destroyed the subscription still survives. We need to ensure the subscription gets unsubscribed when the component gets destroyed.

To unsubscribe on component tear down we use a Subject in combination with the takeUntil operator. Our Subject fires and completes as soon as the component gets destroyed. Our Subject then triggers the takeUntil operator which unsubscribes from our filtered router events.

Great!! We have implemented the refresh functionality. But wait! How does this behave with nested routes?

Distinguish between child route and parent route navigation

Let’s say our routed component defines its own router outlet with routed child components. Guess what happens when we route to a child component?

The router emit the events and we will again fetch the data because our routed component and its subscription are still alive.

To handle nested routes we somehow need to detect if the user routed to the parent or to the child route.

Rx’s pairwise operator emits an array of the previous NavigationEnd and the current NavigationEnd which allows us to compare the URL’s and to distinguish between parent and child routing.

A match between the previous and the current URL represents a refresh. This is the only case we want to fetch data again.

Notice that we kick off the stream by using startWith. Due to the pairwise operator the initial event would never emit without the startWith operator. No data would be fetched.

Pros of the route events approach 👍

  • Component decides when to fetch data.
  • Declarative approach.
  • Decoupling parent and routed component.

Cons of the route events approach 👎

  • Handling child routes requires some cumbersome extra logic.
  • Subscription management — we need to remember to unsubscribe the current subscription on destroy. TSLint rules can support us but it is still error prone.

2. Route resolver

We could move our fetch call to a resolver to prefetch data. In some use cases we are even forced to do so. Resolvers ensure that some data is available before our component gets initialized.

As mentioned in the first approach the router does not emit route events on current route refresh by default. When it comes to resolvers this means that the resolvers resolve function doesn’t get called either.

We somehow need to indicate to the Angular router that we want to recall this function. To get started, we again need to set the onSameUrlNavigation to reload.

But that’s yet not enough. While this change emits router events, the resolve function is not called again. In addition to the onSameUrlNavigation property we also need to set the runGuardsAndResolvers property on our route accordingly.

The runGuardsAndResolvers property has 3 possible values.

  • paramsChange: fires only when route params have changed
  • paramsOrQueryParamsChange: fires only when a query param or a route param changes
  • always: Always fires on navigation to the route

The combination of the onSameUrlNavigation and the runGuardsAndResolvers property is what gets us going. In our example we set runGuardsAndResolvers to ‘always’.

This will recall our resolve function. The resolver is only called when we route to our RoutedComponent and therefore no filtering is required. But again, what about child routes?

Resolvers and child routes

Child routes will also trigger our resolvers function. Again, some manual check is required. We need to find out if the current route is refreshed.

Inside the resolver we assign the previous url to an internal property. On the next execution of the resolve function we then check if we want to refresh the current route or if a child navigation happened.

Click here to tweet about this article 🐥

In case of refresh we fetch the data. In case of child navigation we return an Observable that never emits.

Pros of the resolver approach 👍

  • No subscription management.
  • Decoupling parent and routed child.

Cons of the resolver approach 👎

  • Handling child routes requires some cumbersome extra logic.
  • Resolvers may introduce worse user experience — it is often better to already display some components layouts and a loading indicator while the data is fetched.

3. Refresh timestamp as queryParam

In this approach we do not change the router behaviour at all. Which means that we don’t emit events on same route navigation. Instead we add a refresh query parameter that contains a timestamp of the current time.

this.router.navigate(['/routedComponent'], {
queryParams: {refresh: new Date().getTime()}
});

Inside our component we subscribe to the queryParamMap and refetch data when the next handler is called.

Pros of including refresh queryParam 👍

  • Router behaviour stays untouched.
  • No extra logic to distinguish between parent and child routing.

Cons of including refresh queryParam 👎

  • URL changes — contains refresh parameter.
  • URL params may be confusing when copied and send to another person. Value of refresh property does not match with the time we actually fetched the data.

4. Call Method on routed component

Angular’s ViewChild annotation allows you to get a hold of child components and call methods on it.

Even though your routed component exists in the DOM you can not access it via ViewChild.

But the router-outlet emits an activate event with the activated component. We react to this event to get hold of the currently activated component.

<router-outlet (activate)="setRoutedComponent($event)">
</router-outlet>

Inside our main component we can store it in the following way:

private routedComponent: ExampleRouteComponent;
public setRoutedComponent(componentRef: ExampleRouteComponent){
this.routedComponent = componentRef;
}

When the navigation is clicked we then need to find out if a refresh happened. This check allows us to distinguish between the initial routing and the refresh.

On first routing we just normally navigate to our component. The component then is responsible to fetch the data.

As soon as the component gets activated the router fires the activated event. We use this event and assign the activated component to an internal property.

When we search again our logic will detect that the route has not changed and will call refresh on the activated component. The refresh will then refetch the data.

Refreshing multiple Routes with Refreshable interface

In case we have multiple routes that need to be “refreshable” it is quite helpful to have a common interface for those components. Having this interface allows us to improve readability and type safety.

export interface Refreshable {
refresh: () => void;
}

Our component implements the Refreshable interface and specifies what to do when refresh is called.

export class ExampleRouteComponent implements Refreshable {
   refresh(){
fetchData();
}
}

In our main component we can then also use the Refreshable interface for our activated components.

private routedComponent: Refreshable;
public setRoutedComponent(componentRef: Refreshable){
this.routedComponent = componentRef;
}

Pros of calling child methods 👍

  • Router behaviour stays untouched.
  • No extra logic to distinguish between parent and child routing

Cons of calling child methods 👎

  • Coupling — Parent needs to know its children (or at least which method does the refresh).

What to use?

All the illustrated possibilities have their advantages and disadvantages. Pick the appropriate solution based on your use case.

I personally take advantage of the routers activate event in case I have nested routes. For routes without children I use the first approach where I filter out the router events.

Tomas Trajan, thanks a lot for your feedback!

🧞‍ 🙏 By the way, click (up to 50x) on the 👏🏻 clap 👏🏻button on the left side if you enjoyed this post.

Claps help other people finding it and encourage me to write more posts

Feel free to check out some of my other articles about frontend development.