angular-architects / module-federation-plugin

MIT License
699 stars 190 forks source link

Is it possible to share the NgRx store from the host application to the remotes? #11

Closed KyleThen closed 3 years ago

KyleThen commented 3 years ago

Little background, I am trying to breakup our monolith UI into separate buildable and deployable pieces. Module federation seems like a perfect use case for this.

Problem is, in our current application, there is a shared application state that a lot of the would be micro-frontends rely on. The micro-frontends also have their own piece of the store for their application specific state.

I use StoreModule.forFeature in the remote apps, but get an error NullInjectorError: No provider for ReducerManager! which is provided when you do StoreModule.forRoot, however, I don't necessarily want to define another root store in the remote app. Is it possible to share the store state with all micro-frontends?

o-b-one commented 3 years ago

Hi @KyleThenTR,

you can do so, I done it in a simple POC where I've simple empty object state as the forRoot for the shell application and reducers are set as forFeature in my remote applications

so you can see the shell application has simple AppModule with routings are store initialization like that:

@NgModule({
  imports: [
    BrowserModule,
    RouterModule.forRoot(APP_ROUTES),
    StoreModule.forRoot({}),
    environment.production ? [] : StoreDevtoolsModule.instrument({})
  ],
  declarations: [
    AppComponent,
    NotFoundComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

and on the remove feature modules that I'm loading with module-federation I'm initiating the feature reducer like that:

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild(FLIGHTS_ROUTES),
    StoreModule.forFeature('flight', TestReducer),
    SharedLibModule,
    PortalModule
  ],
  declarations: [
    HomeComponent,
    FlightsSearchComponent
  ],
  exports:[CommonModule]
})
export class FlightsModule { }

Querying the store is done in a normal way with selectors:

@Component({
  selector: 'flights-home',
  templateUrl: './home.component.html'
})
export class HomeComponent implements OnInit {
  public score$: Observable<number>;

constructor(
    private store: Store
  ) {
 this.score$ = this.store.pipe(select(scoreSelector));
  }

  }
parikshit1206 commented 3 years ago

Hello @o-b-one can you share your POC repo.

Thanks

krzkz94 commented 2 years ago

For reference, if you want to go this approach make sure to "share" @ngrx/store in webpack.config.ts for any microFrontend consuming it as well as for the shell application.

If you don't do this, you will get a NullInjectorError: No provider for ReducerManager! unless you provide the root store as well in the microApp.

Appius commented 2 years ago

Hey @o-b-one, I would really appreciate if you share a POC if you still have it.

I am stuck with NullInjectorError: No provider for ReducerManager! error for the past week and cannot get rid of it. I am totally sure that I am "sharing" all @angular/* and @ngrx/* dependencies in both host and remote microfrontends. But nevertheless remote application cannot be bootstrapped without Store.ForRoot() call (it registers RedecerManager there).

@krzkz94, did you really just add ngrx store into share in webpack config and it works without calling .ForRoot?

o-b-one commented 2 years ago

@Appius I used forRoot as it's a must, so you will need to:

  1. Add the ngrx/* to webpack share
  2. Call StoreModule.forRoot({}) on the shell app module
  3. Create feature using StoreModule.forFeature or featureCreator on your mfe

you can see this POC project

Appius commented 2 years ago

Hi @o-b-one, Thanks for answering!

you can see this POC project

Actually, that's exactly what I DON'T want to do. In this POC, every 'feature' microframework eventually do the following:

@NgModule({
  imports: [
    ...
    StoreModule.forRoot({}),
    StoreModule.forFeature(feedFeatureKey, feedReducer),
    ...
  ],
  providers: [],
  bootstrap: [AppComponent],
})

This way, every microfrontend will have ITS OWN store and will not share it with shell application.

What I want to do is to have only ONE store, only ONE call StoreModule.ForRoot() across all microfrontends.

I have done a very small poc how to reproduce this based on well-known https://github.com/manfredsteyer/multi-framework-micro-frontend:

My repo - https://github.com/Appius/multi-framework-micro-frontend Commit with all my changes: a3cff904820c512585fadd4121843cd154963590

All changes are super easy - adding ngrx to package json, to webpack config and registering store with StoreModule.forRoot() and StoreModule.forFeature().

And after these changes, I got the same error NullInjectorError: No provider for ReducerManager!.

Also, I tried to modify webpack share to include singleton: true and eager: true to all @ngrx/* packages, but no luck!

o-b-one commented 2 years ago

@Appius In the POC I shared each application doesn't have its own store as eventually it's just calling forFeature (via the BootstrapModule - consumed by the shell via module-federation) and not forRoot (via AppModule for dev purposes) please raise if I missed something here.

That way you get: Shell --------------> MFE Bootstrap Module
MFE AppModule---^

That means you need to have:

  1. Shared @ngrx/* with singleton: true
  2. StoreModule.forRoot({}) on you Shell AppModule
  3. StoreModule.forFeature() on each of your micro-frontend BootstrapModule (Not the AppModule) on your

Few suggestions from my own experience with Micro-frontend and module-federation:

  1. Don't use eager: true as it will increase the remoteEntry.js while injecting all dependencies.
  2. Don't interact with your micro-frontends store (or any other "storage layer") directly, wrap it with a singleton service (aka facade) and expose it. That way you will be able to make each application decoupled from another, fully isolated with the ability to avoid breaking changes and with the ability to refactor and change underly level implementations (NgRX to Apollo GraphQL, change of business logic, avoid race conditions e.g.). - You can read more on the article I wrote
Appius commented 2 years ago

@o-b-one, thank you very much for help, I made it work!

ori-maci commented 2 years ago

The solution by @o-b-one worked for me. However, I also needed to add the following to my MFE and shell/consumer so the shell did not have the Error: No provider for ReducerManager

shared: share({
    "@ngrx/store": {
      requiredVersion: 'auto',
      singleton: true
    },
    "@ngrx/effects": {
      requiredVersion: 'auto',
      singleton: true
    },

    ...sharedMappings.getDescriptors()
  })
Bryelmo commented 1 year ago

Hi @o-b-one, I'm new on microfrontend architecture. I set the state and relatives actions, reducers and selectors into the Shell app and my goal is share the state with the other remote apps.

Can you give me a reference for achieve this goal? Because I have some questions and I'm looking for answers.

For example: StoreModule.forFeature('flight', TestReducer),

Thank you so much.

JacobSiegle commented 1 year ago

Had issues where this worked locally, but when deployed the MFE would still NullInjectorError: NullInjectorError: No provider for e! which seemed to tie back to Error: No provider for ReducerManager. Upgraded to ng14 LTS (14.2.12) and that seemed to fix that error if anyone else is still having issues.

albmafini commented 1 year ago

Hi @o-b-one, I'm new on microfrontend architecture. I set the state and relatives actions, reducers and selectors into the Shell app and my goal is share the state with the other remote apps.

Can you give me a reference for achieve this goal? Because I have some questions and I'm looking for answers.

For example: StoreModule.forFeature('flight', TestReducer),

* Where TestReduces comes from?

* Does remote app need also ngrx library in its package.json?

Thank you so much.

Have you made any progress here?

Have questions myself.

o-b-one commented 1 year ago

@Bryelmo @albmafini

I have been working with MFE for quite a long time. I can share from my experience that decoupling must be found to achieve proper independent MFEs.

Decoupling of MFEs achieved by:

  1. MFE is self-contained - You can run your MFE without any additional shell, which means you are not coupled with any external source
  2. MFE should be the owner of its stability by automated tests coverage - The last thing we want to get is someone released a new version of his MFE and now another MFE is set on fire.
  3. MFE should be served as a black box - While consumers are not aware of the underly implementation details, your MFE can change its business logic, models, and more without worry about breaking changes (embrace encapsulation). That means you should expose your MFE data (and a Store if you are using one) via formal APIs (aka. services/utils). More about it here.
  4. Bonus - Feature-flag changes in MFEs

About your questions: * Where TestReduces comes from? - Following bullets # 1 and # 2 - the MFE itself

* Does remote app need also ngrx library in its package.json? - Following bullet # 1 - the MFE itself, but you should put it in the shared to avoid double load of NgRX and multiple instances of the store. Angular example (Please avoid using shareAll) React example

Hope I was able to help, keep rocking!

SanderBreivik commented 1 year ago

@o-b-one Why do you not recommend using shareAll in angular?

o-b-one commented 1 year ago

@o-b-one Why do you not recommend using shareAll in angular?

In general, and not just in Angular, for few reasons:

  1. Increase page load assets - shareAll makes Webpack to generate for each one of your project dependencies (based on package.json) an asset. Meaning, that for your application to be bootstrapped, all those assets will need to be downloaded and load. That makes vendors.js size to be reduced while producing many additional scripts (per dependency). This will cause the dependencies code not to be completely optimized (like cross dependencies code duplication reduction). Also, in case you are not using HTTP2 this going to increase your page load time
  2. Coupling - having shareAll means you are aiming to be able to use all you dependencies as already loaded by other application, an mainly the shell. Are you sure you want your micro-app to relay on the development lifecycle of other apps? Assume you want to upgrade a dependency version, would you like to consider upgrading it across all micro-apps? What will be the cost for such procedure? Having say that, there are some packages which this kind of upgrade is inevitable without changing the micro-app load strategy like ngrx and Angular.
kuldeepGDI commented 10 months ago

I do not get it though for us it was working fine until angular 13 and MF 14 versions. Now it stopped working. I made changes based on the suggestion here, and this seems to be working for me, but still i am not sure how it was working and what broke it. So my expectation still would be to not needing to do Store.forRoot in shell UI because what if shell UI loads many other MFEs which are not using ngrx store. I would still prefer to see a way where we could still share the store (in angular 13 with MF 14 we were simply using Store.forRoot in our exposed MFCs), although I can see that it is not recommended practice to use forRoot twice in the app, but as long as the modules is purely used for shell app, it should be fine ?

ArgV04 commented 1 month ago

@o-b-one Why do you not recommend using shareAll in angular?

@o-b-one how a right configuration would look like without sharing all?

Our scenario is like this:

we are working in a Nx Monorepo having multiple micro frontends in place and shell applications. Because of all apps are related to the same package.json all having the same dependencies in version context.

The thing is that outside of our repo there are other applications which also have an interest of consuming our micro frontends. And here we have no knowledge what dependency in version context they have.

For this I was thinking we need some kind of configuration if outside consumers having same versioned deps we sharing them and if not the apps are loading their own deps if they are not fit from version point of view.