single-spa-angular: Problems with mounting the same parcelConfig multiple times

Demonstration

const parcelConfig = singleSpaAngular({...})

const parcel1 = mountRootParcel(parcelConfig, {...})
const parcel2 = mountRootParcel(parcelConfig, {...})

Expected Behavior

When you mount the same single-spa-angular parcelConfig multiple times, multiple independent parcels should be created.

Actual Behavior

When you mount the same single-spa-angular parcelConfig multiple times, only one of them really works. See more at https://single-spa.slack.com/archives/CGETM8T5X/p1593605815279300

This is caused by the opts object being shared between the parcels, but it containing singleton properties. The properties should be changed to be objects/arrays that allow for multiple values (one for each parcel). Specifically opts.bootstrappedModule should not be a single value, but an array/object with multiple values.

This bug also existed in single-spa-react and was fixed in https://github.com/single-spa/single-spa-react/pull/68. The history of it is that many of the single-spa helper libraries were authored at a time when only single-spa applications existed, instead of parcels. Applications are indeed singletons, which is why there is no issue for them.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Comments: 18 (6 by maintainers)

Most upvoted comments

I was able to solve the given problem with no changes to the library.

  1. Use ngDoBootstrap in app.module.ts
import {ApplicationRef, DoBootstrap, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {AppComponent} from './app.component';

export const AppModule = (id: string) => {
  @NgModule({
    declarations: [AppComponent],
    imports: [BrowserModule],
    providers: [],
  })
  class AppModule implements DoBootstrap {
    constructor() {}
    ngDoBootstrap(appRef: ApplicationRef) {
      appRef.bootstrap(AppComponent, `example-root-${id}`);
    }
  }
  return AppModule;
}
  1. You should upgrade file main.single-spa.ts
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';

if (environment.production) {
  enableProdMode();
}

export const configOptions = (id: string) => ({
  bootstrapFunction: (singleSpaProps: any) => {
    singleSpaPropsSubject.next(singleSpaProps);
    return platformBrowserDynamic().bootstrapModule(AppModule(id));
  },
  template: `<example-root-${id} />`,
  NgZone,
});
  1. And load parcel with System.import
public mount(appName: string, domElement: HTMLElement, id: string = '1'): Observable<unknown> {
        return from(System.import(appName)).pipe(
            tap((app: { configOptions: (id: string) => SingleSpaAngularOptions }) => {
                const lifecycles = singleSpaAngular(app.configOptions(id));
                mountRootParcel(lifecycles, { domElement });
            })
        );
    }

Working with single-spa-react, my team and I encountered an issue where we couldn’t instantiate multiple parcels on the same page using another parcel as a wrapper. After conducting extensive research, we discovered that using the wrapWith prop of the Parcel component, as described in the single-spa-react documentation, resolved the problem. By wrapping our parcels with a simple div, we were able to successfully instantiate multiple identical parcels inside a parcel wrapper without any issues.

I believe that our solution using wrapWith in single-spa-react could contribute some ideas to address the problem, or at least prompt an exploration of the wrapWith prop.