Why would you use a resolver with Angular

AngularAngular Resolver

Angular Problem Overview


I like the idea of resolvers.

You can say that:

  • for a given route you expect some data to be loaded first
  • you can just have a really simple component with no observable (as retrieving data from this.route.snapshot.data)

So resolvers make a lot of sense.

BUT:

  • You're not changing the URL and displaying your requested component until you receive the actual response. So you can't (simply) show the user that something is happening by rendering your component and showing as much as you can (just like it's advised to, for the shell app with PWA). Which means that when having a bad connection, your user may have to just wait without visual indication of what's happening for a long time
  • If you are using a resolver on a route with param, let's take as an example users/1, it'll work fine the first time. But if you go to users/2, well nothing will happen unless you start using another observable: this.route.data.subscribe()

So it feels like resolvers might be helpful retrieving some data BUT in practice I wouldn't use them in case there's a slow network and especially for routes with params.

Am I missing something here? Is there a way to use them with those real constraints?

Angular Solutions


Solution 1 - Angular

Resolver: It gets executed even before the user is routed to new page.

Whenever you need to get the data before the component initialization, the right way to do this is to use resolver. Resolver acts synchronously i.e. resolver will wait for async call to complete and only after processing the async call, it will route to the respective URL. Thus, the component initialization will wait till the callback is completed. Thus, if you want to do something (service call), even before the component is initialized, you have come to right place.

Example Scenario: I was working on the project where user would pass the name of file to be loaded in the URL. Based on the name passed we would do the async call in ngOnInit and get the file. But the problem with this is, if user passes the incorrect name in URL, our service would try to fetch the file which does not exists on the server. We have 2 option in such scenario:

Option 1: To get the list of valid filenames in ngOnInit, and then call the actual service to fetch the file (If file name is valid). Both of these calls should be synchronous.

Option 2: Fetch the list of valid filenames in the resolver, check whether filename in URL is valid or not, and then fetch the file data.

Option 2 is a better choice, since resolver handles the synchronicity of calls.

Important:: Use resolver when you want to fetch the data even before the user is routed to the URL. Resolver could include service calls which would bring us the data required to load the next page.

Solution 2 - Angular

This is why I would use a resolver:

  • Single Responsibility, my component needs some data, in some cases this data are provided by a cache or state, when missing I need first to fetch them, and pass directly, without changing my component. Example: A Search Result page with a list myapp.com/lists, navigate to one element of the list myapp.com/lists/1, showing the details of that element, no need to fetch the data, already done by the search. Then let suppose you navigate directly to myapp.com/lists/1 you need to fetch and then navigate to the component
  • Isolate your component from router logic
  • My component should not manage the loading state of a fetch request, it's only responsibility is showing the data

Think to the resolver as a middleware between your app and your component, you can manage the loading view in the parent component, where <router-outlet> is included.

Solution 3 - Angular

The resolver gives you a hook near the start of router navigation and it lets you control the navigation attempt. This gives you a lot of freedom, but not a lot of hard and fast rules.

You don't have to use the result of the resolve in your component. You can just use the resolve as a hook. This is my preferred way of using it for the reasons you've cited. Using the result in your component is much simpler, but it has those synchronous trade-offs.

For example, if you're using something like Material or Cdk, you could dispatch a "loading" dialog to display a progress indicator at the start of the resolve, then close the dialog when the resolve concludes. Like so:

constructor(route: ActivatedRouteSnapshot, myService: MyService, dialog: MatDialog) {}

resolve() {
    const dialogRef = this.dialog.open(ProgressComponent);
    return this.myService.getMyImportantData().pipe(
        tap(data => this.myService.storeData(data)),
        tap(() => dialogRef.close()),
        map(() => true),
        catchError(err => of(false))
    );
}

If you're using ngrx, you could do something similar by dispatching actions while the resolve is in progress.

When you're using the Resolve guard in this fashion, there's not a particularly strong reason to use the Resolve guard instead of the CanActivate guard. That choice comes down to semantics. I find it more obvious to gate authentication on CanActivate and initiate data retrieval from Resolve. When those topics are allowed to blend it becomes an abritrary choice.

Solution 4 - Angular

I've been wondering the exact same thing.

The most intuitive discussion of this topic I've encountered is here: https://angular.schule/blog/2019-07-resolvers.

The author essentially boils it down to this: if you already use resolvers and don't have any UX issues, go for it. But most of the time, resolvers add unnecessary complexity and you're better off going with a reactive approach using a "smart containers" and "presentational components" structure. There are very few exceptions.

Using this structure, the smart components serve as a more dynamic form of resolver, and your presentation component handles the display of the pseudo-synchronous data.

As far as I can tell, resolvers are essentially a crutch for those who are less comfortable working with reactive patterns.

Solution 5 - Angular

I actually currently refactoring an app which uses a lot of resolvers, thought about them a lot and think the biggest issue with them is that they mutate the data, you have to map the data fetched from activatedRoute. In other words it adds complexities and problems in maintaining the app, whereas direct service injection doesn't have this issue.... Furthermore because resolvers are synchronous, in most cases the really slow down user experience...

Solution 6 - Angular

Angular route data resolvers are hooks into the router's navigation event chain that help to provide data that is need starting from a parent route (inclusive) and down to all its child routes.

From the official Angular Router documentation, this is the order in which router events happen:

  1. NavigationStart: Navigation starts.
  2. RouteConfigLoadStart: Before the router lazy loads a route configuration.
  3. RouteConfigLoadEnd: After a route has been lazy loaded.
  4. RoutesRecognized: When the router parses the URL and the routes are recognized.
  5. GuardsCheckStart: When the router begins the guards phase of routing.
  6. ChildActivationStart: When the router begins activating a route's children.
  7. ActivationStart: When the router begins activating a route.
  8. GuardsCheckEnd: When the router finishes the guards phase of routing successfully.
  9. ResolveStart: When the router begins the resolve phase of routing.
  10. ResolveEnd: When the router finishes the resolve phase of routing successfuly.
  11. ChildActivationEnd: When the router finishes activating a route's children.
  12. ActivationEnd: When the router finishes activating a route.
  13. NavigationEnd: When navigation ends successfully.
  14. NavigationCancel: When navigation is canceled.
  15. NavigationError: When navigation fails due to an unexpected error.
  16. Scroll: When the user scrolls.
So why are resolvers needed?

Single Responsibility Principle (SRP) and DRY (Don't Repeat Yourself). Data fetching (and/or caching) is normally implemented in a service, while a resolver selects which data to provide from which service(s) and even if a resolver is synchronous, if the data is cached, the user won't notice a lag.

Example

Data can be fetched (resolved) once (and possibly cached), at the /items route and be made available as part of the active route data snapshot for all child routes, like /items/:id or /items/:id/edit.

In this example, a list of items is fetched once and when the user needs to edit or view just one item, that item doesn't need to be fetched again because it's already available in the list fetched as part of the /items parent route's resolvers.

Other concerns

To address everybody's concerns that you can't show the user that the app is waiting on some data to load before navigating to a new page, the solution is easy: just hook into the router's events, in your app component for example, and listen to ResolveStart and ResolveEnd events and show or hide a loading animation or loading overlay component or whatever you need to do to let the user know that data is being loading. With mobile apps for example, the pattern used is usually an overlay with a spinner in the center.

EDIT: I have since writing this, come to the conclusion that route resolvers are antipattern. You get much better user experience by navigating immediately to the route the user clicked for and displaying something sooner rather than after a delay, even if the UI will still have to load some data later, you can use a loading indicator in that case.

Solution 7 - Angular

From my point of view, you can use them for simplicity and clarity in the components code, if you don't mind not being able to navigate the user right away to a new section while loading the data.

To improve the UX letting the user know that something is being loaded, you can create a service to keep the loading status. It will subscribe to router events and set a variable to different values depending on what's happening. For example I have used to 3 basic statuses: initial_load, when the app is run for the first time, resolving, while resolving, and done.

Whenever navigation is started or the app is loaded for the first time, the component that contains the <router-outlet> tag will show a loader instead until the loading status is set to done.

For your second question, about navigation from users/1 to users/2, you are completely right. To solve this you would have to subscribe to route parameters changes in the user details component. This doesn't usually happen in the apps that I've worked with, which have a table where you select a user, and to select another one you would have to navigate back to the listing.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionmaxime1992View Question on Stackoverflow
Solution 1 - Angularharsh hundiwalaView Answer on Stackoverflow
Solution 2 - AngularalbanxView Answer on Stackoverflow
Solution 3 - AngularJohn Christopher JonesView Answer on Stackoverflow
Solution 4 - AngularGregView Answer on Stackoverflow
Solution 5 - AngularTimothyView Answer on Stackoverflow
Solution 6 - AngularPaul-SebastianView Answer on Stackoverflow
Solution 7 - AngularPizzicatoView Answer on Stackoverflow