single-spa / single-spa-angular

Helpers for building single-spa applications which use Angular
Apache License 2.0
202 stars 78 forks source link

Navigation sometimes gets cancelled whenever a route change occurs on initial load #334

Open stuartcargill opened 3 years ago

stuartcargill commented 3 years ago

Hi

I have a large application which runs 2 Angular MFE’s at the same time (nav mfe and a primary mfe)

This app has dynamic email links sent out to users. Upon opening these links, both angular mfe's bootstrap and the primary mfe calls an API to figure out what action the user wants to take (.e.g a password reset or email verify). The API call is quite lightweight and navigates the user to the respective view within the primary app.

https://myapp.com/email-handler?actioncode=123 -> https://myapp.com/reset-password

I have noticed that there is an intermittent issue with the routing where it will get stuck on the email-handler path. The redirect to the next view does not happen. The issue occurs around 1/15 attempts but seems to be more prominent when the user is on a slower connection.

After doing some tracing I can see that the navigation event gets cancelled because angular is expecting a different navigation id than the one being proposed: See below for router trace:

image

My understanding is, single-spa emits a popstate event every time a route is changed to tell MFE’s to re-render. If I set the urlReRouteOnly flag to true, this issue doesn’t happen anymore.

Whenever Angular sees a popstate event being fired, it schedules a navigation event to occur. In single-spa-angular, skipNextPopState helps us to ignore popstate events that were not dispatched by the browser (ie single-spa). What I’ve seen is sometimes this flag is set to false and thus a popstate is emitted. This triggers Angular’s navigation and it writes a new navigation id to the History API. Depending on the timing of the navigation being scheduled, Angular expects a certain navigation id but sees a different one so it cancels the navigation event.

It seems to be timing related - whenever a successful route change occurs, I can see the popstate events are emitted at a different time compared to when the navigation gets cancelled.

Demonstration As this is a large complex app with many modules, views & dependencies, it is very difficult to create a like-for-like sample app to replicate the issue. (If I manage to replicate it, I will upload it)

Summary

I’ve tried to narrow down the issue further but to no avail. Any help or advice is appreciated.. thanks guys.

Angular Version - 9.1 (Tested on the latest version of single-spa-angular)

arturovt commented 3 years ago

I'm not sure we can do anything w/o reproducible example. In general, I do not understand something very well from the screenshots.

Well, the navigation gets cancelled when somebody calls navigateByUrl (and not because navigation id doesn't match, the navigation id matching happens at the very end). It's called manually during the initial navigation by Angular when it bootstraps the module for the first time and when popstate event is dispatched. However, we can't do anything with initial navigation since it's called internally.

As I understand the navigation is cancelled for the specific app where some view should be rendered. What I would do is open the router file (node_modules/@angular/router/__ivy_ngcc__/fesm2015/router.js), press Ctrl+F and search for setupNavigations. You should be able to find this:

setupNavigations(transitions) {
        const eventsSubject = this.events;
        return transitions.pipe(filter(t => t.id !== 0), 
        // Extract URL
        map(t => (Object.assign(Object.assign({}, t), { extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl) }))), 

There is switchMap going right after the map and it has an appropriate comment:

// Using switchMap so we cancel executing navigations when a new one comes in

You can modify that map to get the extracted URL before returning it so you'll understand what URL navigation cancels the current navigation:

// Extract URL
        map(t => {
            const url = Object.assign(Object.assign({}, t), { extractedUrl: this.urlHandlingStrategy.extract(t.rawUrl) });
            console.log('url = ', url);
            return url;
        }), 
stuartcargill commented 3 years ago

@arturovt - I tried your suggestion but couldn't figure out the issue. In the end up, I converted my nav MFE from Angular to React and this fixed my problem..