platform icon indicating copy to clipboard operation
platform copied to clipboard

No Provider issue on lazy loaded module and ViewContainerRef with Angular 14

Open Julienbideau opened this issue 2 years ago • 44 comments

Which @ngrx/* package(s) are the source of the bug?

router-store

Minimal reproduction of the bug/regression with instructions

Since we bumped versions from Angular V13 to V14, and ngrx 13.0 to 14.2, we have an error : NullInjectorError: NullInjectorError: No provider for Store!

We need to declare our Store in a lazy loaded module :

StoreModule.forRoot({}),
EffectsModule.forRoot([])

We also don't use routing in our app, just component initialization like so :

 @ViewChild('transactionContainer', { read: ViewContainerRef, static: true })
  public transactionContainer: ViewContainerRef | undefined;

  constructor(private readonly injector: Injector) {}

  async loadLazyComponent() {

    const lazyStoreModuleNgModuleRef = createNgModule(
      LazyStoreModule,
      this.injector
    );
    this.transactionContainer!.createComponent(LazyStoreComponent, {
      ngModuleRef: lazyStoreModuleNgModuleRef,
    });
  }

If I declare the store in the app.module.ts, It solves the problem but we can't declare the store there because we need our stores to be isolated in each component

I made a stackblitz : https://stackblitz.com/edit/angular-ivy-u9b5yo?file=package.json

If you downgrade to 13.2.0, we have no issues anymore.

Minimal reproduction of the bug/regression with instructions

The store to be correctly provided

Versions of NgRx, Angular, Node, affected browser(s) and operating system(s)

NgRx: 14.3.2 Angular 14.2.12 Node 16

Other information

Thank you in advance ! :-)

I would be willing to submit a PR to fix this issue

  • [X] Yes
  • [X] No

Julienbideau avatar Dec 06 '22 19:12 Julienbideau

We have the same problem in our project. Are there any news?

Phusty avatar Jan 11 '23 09:01 Phusty

Same Problem on our side. Please let us know. Thanks

rudobri avatar Jan 16 '23 11:01 rudobri

I tryed the 15.1.0 version and we still have the same problem

Julienbideau avatar Jan 17 '23 10:01 Julienbideau

Just ran into this as well. Anyone solve it yet?

Simon-IEEECS avatar Jan 19 '23 10:01 Simon-IEEECS

We did not change anything with how injection works between those versions so it could have been a change in Angular. Will look into it though

brandonroberts avatar Jan 19 '23 13:01 brandonroberts

We experienced this issue with the 14.3.0 version, we solved it by downgrading to the 14.2.0 version. Maybe it could be related to the change on how the services are provided. Hope it helps!

vavon92 avatar Jan 19 '23 15:01 vavon92

Thanks @vavon92. That seems like a reasonable workaround for now. In 14.3.x, we did change where the providers are not inlined directly in the providers array to add support for standalone provider APIs, but to me, that should not have caused a breakage here.

brandonroberts avatar Jan 19 '23 16:01 brandonroberts

Indeed, a breaking change was introduced in v14.3 in case root store providers are not provided at the root injector level (in AppModule or another eagerly loaded module).

To fix this issue, move the StoreModule.forRoot call to AppModule.imports: https://stackblitz.com/edit/angular-ivy-den6xd?file=src%2Fapp%2Fapp.module.ts

markostanimirovic avatar Jan 19 '23 22:01 markostanimirovic

That's a temporary fix right? The whole point of lazy loading is that I don't need the store in my other modules

Simon-IEEECS avatar Jan 19 '23 22:01 Simon-IEEECS

That's a temporary fix right? The whole point of lazy loading is that I don't need the store in my other modules

I hope so, we can't update to the v15 otherwise! It's quite blocking on our side as we will update to angular 15 soon (requiring v15 for ngrx)

vavon92 avatar Jan 20 '23 06:01 vavon92

@vavon92 I just tried to downgrade to 14.2.0 in my stackblitz and I still have the issue

Julienbideau avatar Jan 20 '23 16:01 Julienbideau

@Julienbideau We tested in on our production environment and it works, I don't have any more insights on that sorry Maybe you could try downgrading to the 14.0.0 and see if that works out for you

vavon92 avatar Jan 23 '23 14:01 vavon92

@Julienbideau Checkout your package-lock.json file, make sure you have installed 14.2.0 version.

If you run ng update it will install 14.3.3 version, I fixed this with npm install instead.

benj0c avatar Jan 27 '23 11:01 benj0c

I have a very large application that I'm going to incrementally move over to the new Angular paradigms and I'm running into this same issue. I have some common state shared between lazy loaded features and I declare that via imports in the application NgModule using StoreModule.forRoot(). Then each older lazy loaded feature uses StoreModule.forChild() which works fine. I started creating a new feature module today using standalone components, routes with providers, and provideState() and as soon as I navigate to that route, I get the same error as above: NullInjectorError: NullInjectorError: No provider for InjectionToken @ngrx/store Root Store Provider!.

My app is using Angular 14 and ngrx 14, but I was able to put together a small-ish reproduction to demonstrate this is still happening in Angular 15 and ngrx 15:

https://stackblitz.com/edit/angular-wzciju

bryanforbes avatar Feb 23 '23 22:02 bryanforbes

I guess the issue comes from the new provide_store.ts added in 14.3.0 https://github.com/ngrx/platform/compare/14.2.0...14.3.0

I didn't found the issue yet

Julienbideau avatar Feb 24 '23 09:02 Julienbideau

@Julienbideau I haven't either. I suspect it's a race condition somewhere.

bryanforbes avatar Feb 24 '23 14:02 bryanforbes

@bryanforbes It's not a race condition. In your case, you're mixing two different ways of registering the Store. provideStore has providers that StoreModule.forRoot() does not have.

This works

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { provideStore, StoreModule } from '@ngrx/store';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { commonReducer, commonFeatureName } from './common';

@NgModule({
  declarations: [AppComponent],
  imports: [
    CommonModule,
    BrowserModule,
    AppRoutingModule,
    // StoreModule.forRoot({ [commonFeatureName]: commonReducer }), <-- Does not work with provideState()
  ],
  providers: [
    provideStore({ [commonFeatureName]: commonReducer }), // works with provideState()
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

brandonroberts avatar Feb 24 '23 15:02 brandonroberts

@brandonroberts Thank you for the quick response! Should StoreModule.forFeature() work with provideStore() at the root level? I have several feature modules that use StoreModule.forFeature() and I'm trying to avoid rewriting older code (I'd like to do it incrementally when I have time) as well as avoiding providing two stores.

bryanforbes avatar Feb 24 '23 16:02 bryanforbes

@bryanforbes yes, it should work also. provideStore() and includes all the previous providers and tokens, and a few new ones to support environment providers.

brandonroberts avatar Feb 24 '23 16:02 brandonroberts

@brandonroberts I forked my previous stackblitz, added some feature state to hello.module.ts, switched to using provideStore() at the root, and I'm now getting an injector error when loading the hello route: NullInjectorError: NullInjectorError: No provider for StoreRootModule

https://stackblitz.com/edit/angular-9lezro

bryanforbes avatar Feb 24 '23 16:02 bryanforbes

@bryanforbes ahh yes, that's correct because of the NgModules. You can put both in your AppModule, all the providers will get merged, so last one wins and you'll get all the necessary dependencies. The state only needs to be registered once though in provideStore().

@NgModule({
  declarations: [AppComponent],
  imports: [
    CommonModule,
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot(),
  ],
  providers: [provideStore({ [commonFeatureName]: commonReducer })],
  bootstrap: [AppComponent],
})
export class AppModule {}

brandonroberts avatar Feb 24 '23 16:02 brandonroberts

@brandonroberts Perfect! Thanks for helping me debug that. It might be good to add that as an FAQ in the docs.

bryanforbes avatar Feb 24 '23 16:02 bryanforbes

@brandonroberts will I need to do something similar with EffectsModule.forRoot() and provideEffects() in the app module?

bryanforbes avatar Feb 24 '23 17:02 bryanforbes

@bryanforbes most likely yes, same rule applies though with only registering the root effects once if you have any

brandonroberts avatar Feb 24 '23 17:02 brandonroberts

A breaking change was introduced in v14.3. I am still curious how come it was working in the early version because it shouldn't. It is recommended to only import StoreModule.forRoot({}) in the root module of your Angular application, not in a lazy-loaded module. This is because StoreModule.forRoot({}) registers the store singleton with the root injector, and you don't want to create multiple instances of the store in your application.

if you want to create a lazy loaded store module for the feature.

Add the StoreModule.forFeature() and EffectsModule.forFeature() methods to the imports array of the new module. For example, you can add the following:

imports: [
  CommonModule,
  StoreModule.forFeature('books', booksReducer),
  EffectsModule.forFeature([BooksEffects]),
],

A fix to this issue would be https://github.com/ngrx/platform/issues/3700#issuecomment-1397683026

wahajahmedkhan avatar Mar 31 '23 14:03 wahajahmedkhan

Any news about this issue ?

Alex85651 avatar Jul 10 '23 07:07 Alex85651

@brandonroberts does a solution exist for actual question posed by the OP? For example, we have a host app that lazy loads a remote app using module federation.

The host app does not use NgRx. The remote app does use NgRx.

The goal is for the host app to not know or even care what the remote app is doing for state management (context: remote app is written by a completely separate team working mostly independently).

But currently because of this NullInjectorError, we are forced to include NgRx in the host app and do the StoreModule.forRoot stuff in the AppModule. --> I really don't want this code in the host app. It is irrelevant to the host app and increases the bundle size of the host app, and may not even get used (i.e. remote module may not get fetched), depending on how the user interacts with the app.

I want this lazy loaded remote module to be completely in charge of it's state management concerns, without having to rely on the host app.

Is there any solution for this?

Achilles1515 avatar Sep 21 '23 03:09 Achilles1515

Indeed, a breaking change was introduced in v14.3 in case root store providers are not provided at the root injector level (in AppModule or another eagerly loaded module).

To fix this issue, move the StoreModule.forRoot call to AppModule.imports: https://stackblitz.com/edit/angular-ivy-den6xd?file=src%2Fapp%2Fapp.module.ts

this workaround pretty much is not what one can do i think for Module federation use case where shell UI and module federated components are pretty much independent of each others.

kuldeepGDI avatar Sep 21 '23 05:09 kuldeepGDI

@brandonroberts does a solution exist for actual question posed by the OP? For example, we have a host app that lazy loads a remote app using module federation.

The host app does not use NgRx. The remote app does use NgRx.

The goal is for the host app to not know or even care what the remote app is doing for state management (context: remote app is written by a completely separate team working mostly independently).

But currently because of this NullInjectorError, we are forced to include NgRx in the host app and do the StoreModule.forRoot stuff in the AppModule. --> I really don't want this code in the host app. It is irrelevant to the host app and increases the bundle size of the host app, and may not even get used (i.e. remote module may not get fetched), depending on how the user interacts with the app.

I want this lazy loaded remote module to be completely in charge of it's state management concerns, without having to rely on the host app.

Is there any solution for this?

Even when including StoreModule.forRoot() in the host's app module, we still see the same issue when using module federation. What does your webpack.config look like in regards to the shared libs?

scottbisaillon avatar Oct 11 '23 13:10 scottbisaillon

Even when including StoreModule.forRoot() in the host's app module, we still see the same issue when using module federation. What does your webpack.config look like in regards to the shared libs?

Like this:

let federatedModules = withModuleFederationPlugin({
    shared: share({
       // angular, rxjs, others

        '@ngrx/store': { singleton: true, eager: true, strictVersion: true },
        '@ngrx/effects': { singleton: true, eager: true, strictVersion: true },
    }),
    sharedMappings: [],
});

I believe the remote looks similar, maybe without eager though.

Achilles1515 avatar Oct 11 '23 18:10 Achilles1515