angular-redux / router

Keep your Angular2+ router state in Redux
MIT License
28 stars 7 forks source link

No provider for NgReduxRouter with AoT compilation in Angular 4.0.1 #12

Closed zerefel closed 6 years ago

zerefel commented 7 years ago

Hey there, when making an AoT compilation, the following error is thrown:

ERROR Error: Uncaught (in promise): Error: No provider for NgReduxRouter! Error at injectionError (core.es5.js:1231) [angular] at noProviderError (core.es5.js:1269) [angular] at ReflectiveInjector_._throwOrNull (core.es5.js:2770) [angular] at ReflectiveInjector_._getByKeyDefault (core.es5.js:2809) [angular] at ReflectiveInjector_._getByKey (core.es5.js:2741) [angular] at ReflectiveInjector_.get (core.es5.js:2610) [angular] at AppModuleInjector.NgModuleInjector.get (core.es5.js:3557) [angular] at StoreConfig.get [as reduxRouter] (store.service.ts:36) [angular]

The module works just fine when used in JiT.

Angular version: 4.0.1 Node version node -v: 6.9.2 @angular-redux/router version: 6.1.0

SethDavenport commented 7 years ago

This works with @angular/cli projects: see @angular-redux/example-app for instance. Running that with ng serve --aot works correctly.

I'm guessing you're not using @angular/cli? Can you provide some more details about how you build and bundle your code?

zerefel commented 7 years ago

Thanks for the reply!

My project is based on this repo: https://github.com/angularclass/angular2-webpack-starter It is using webpack to build and bundle the code.

I am running the npm run build:aot:prod command. It basically cleans up the dist folder to prepare it for the new release and then executes webpack --config config/webpack.prod.js.

Unfortunately I cannot link the repo where I can reproduce it, as it is private. However, the way I am injecting the NgReduxRouter service might be causing some trouble:

@Injectable()
export class StoreConfig {
    constructor(private redux: NgRedux<AppState>,
                private injector: Injector
    ) {
    }

    private get reduxRouter(): NgReduxRouter {
        return this.injector.get(NgReduxRouter);
    }

    load(): Promise<any> {
        const store = ...;
        // some async function calls with redux-persist
        // after the store is recovered, initialize redux and the redux router
        this.redux.provideStore(store);
        this.reduxRouter.initialize();
    }
}

Basically, I have a service with a load() method, which is invoked during App Initialization, using the APP_INITIALIZER provider in app.module.ts The reason why I'm injecting it in a getter is due to some case-specific reasons, which I don't want to trouble you with, unless you suspect they could be causing the problem.

drewloomer commented 7 years ago

Did you ever find a solution for this? I'm seeing this error w/ @angular/core 4.1.3 and @angular-redux/router 6.3.0.

zerefel commented 7 years ago

@drewloomer Not yet, I am currently using JiT compilation for my production environment as I'm pressed by time. I will make sure to report here if and when I find a solution.

drewloomer commented 7 years ago

The solution I found was to NOT setup the store and router in the app module constructor, but rather in a separate store module (like in the example app).

store.module.ts

import { NgModule } from '@angular/core';
import { Action } from 'redux';
import { NgReduxModule, NgRedux, DevToolsExtension } from '@angular-redux/store';
import { NgReduxRouterModule, NgReduxRouter } from '@angular-redux/router';
import { createLogger } from 'redux-logger';

import { TestActions } from '../actions';
import { rootReducer } from '../reducers';
import { IAppState, INITIAL_STATE } from './store.model';

@NgModule({
  imports: [NgReduxModule, NgReduxRouterModule],
  providers: [TestActions],
})
export class StoreModule {

  constructor(
    public ngRedux: NgRedux<IAppState>,
    devTools: DevToolsExtension,
    ngReduxRouter: NgReduxRouter
  ) {

    ngRedux.configureStore(
      rootReducer,
      INITIAL_STATE,
      [createLogger()],
      devTools.isEnabled() ? [ devTools.enhancer() ] : []);

    ngReduxRouter.initialize();
  }
}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { NgReduxModule } from '@angular-redux/core';
import { NgReduxRouterModule } from '@angular-redux/router';
import { RouterModule } from '@angular/router';

import { StoreModule } from './store/store.module';
import { routes } from './routes';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    RouterModule.forRoot(routes),
    BrowserModule,
    FormsModule,
    HttpModule,
    NgReduxModule,
    NgReduxRouterModule,
    StoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}
zerefel commented 7 years ago

@drewloomer Unfortunately this does not work for me, because I need the store to be initialized before the Angular application has started. This is why I use the APP_INITIALIZER provider.

app.module.ts

@NgModule({
    bootstrap: [AppComponent],
    declarations: [
        AppComponent
    ],
    imports: [ // Import modules here (NEVER IMPORT LAZY MODULES!!!)
        StoreModule
    ],
    providers: [ // expose our Services and Providers into Angular's dependency injection
        ENV_PROVIDERS,
        APP_PROVIDERS,
        {
            provide: APP_INITIALIZER, // APP_INITIALIZER will execute the function when the app is initialized and delay what it provides.
            useFactory: initStore,
            deps: [StoreConfig],
            multi: true
        }
    ]
})

export function initStore(storeConfig: StoreConfig): Function {
    return () => storeConfig.load();
}

store.service.ts

import { Injectable, Injector, isDevMode } from '@angular/core';

import { NgRedux, DevToolsExtension } from '@angular-redux/store';
import { NgReduxRouter } from '@angular-redux/router';

import { combineEpics, createEpicMiddleware, } from 'redux-observable';
import { createStore, compose, applyMiddleware, Store } from 'redux';
import * as localForage from "localforage";
import { persistStore, autoRehydrate, getStoredState } from 'redux-persist';

import { AppState, rootReducer } from './store';
import { ContentEpics } from './epics/content.epics';
import { UserEpics } from './epics/user.epics';
import { AppEpics } from './epics/app.epics';
import { ExpertsEpics } from './epics/experts.epics';
import { DashboardEpics } from './epics/dashboard.epics';

@Injectable()
export class StoreConfig {
    constructor(private redux: NgRedux<AppState>,
                private injector: Injector,
                private contentEpics: ContentEpics,
                private userEpics: UserEpics,
                private appEpics: AppEpics,
                private expertsEpics: ExpertsEpics,
                private dashboardEpics: DashboardEpics) {
    }

    private get devTools(): DevToolsExtension {
        return this.injector.get(DevToolsExtension);
    }

    private get reduxRouter(): NgReduxRouter {
        return this.injector.get(NgReduxRouter);
    }

    load(): Promise<any> {
        const epics = combineEpics(
            this.contentEpics.epics,
            this.userEpics.epics,
            this.appEpics.epics,
            this.expertsEpics.epics,
            this.dashboardEpics.epics
        );

        let enhancer: any = null;

        if (isDevMode() && this.devTools.isEnabled()) {
            enhancer = compose(
                applyMiddleware(createEpicMiddleware(epics)),
                autoRehydrate(),
                this.devTools.enhancer());
        } else {
            enhancer = compose(
                applyMiddleware(createEpicMiddleware(epics)),
                autoRehydrate());
        }

        const persistConfig = {storage: localForage, whitelist: ['app', 'user'], blacklist: ['content']};

        return new Promise((resolve: any) => {
            getStoredState(persistConfig, (err, state) => {
                const store = createStore(
                    rootReducer,
                    state,
                    enhancer);

                persistStore(store, persistConfig, () => {
                    this.redux.provideStore(store);
                    this.reduxRouter.initialize();

                    resolve(true);
                });
            });
        });
    }
}

I use the Angular Injector to inject the NgReduxRouter and the DevTools enhancers, because if I do not, I get a Cannot instantiate cyclic dependency! ApplicationRef error in the console.