Search code examples
angulartypescriptngrx

Selector doesn't reflect state in reducer


Description

In this Minimal Reproductible Example I don't understand why the reducer has something (we can observe it in the ReduxDevTools) but the selector puts undefined in the component.

Could someone have keys to share about this behavior?

Codes

Here are the parts of the MRE:

  • actions/index.ts
import { createAction } from '@ngrx/store';

export const µAppInitializerEntered = createAction(`[frontend] µAppInitializerEntered`);
  • components/index.html
<pre>{{ feature$ | async | json }}</pre>
  • components/index.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { $feature } from '../selectors';
import { tap } from 'rxjs/operators'

@Component({
    selector: 'workspace-index',
    templateUrl: './index.component.html',
    styleUrls: ['./index.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class IndexComponent {
    feature$ = this.store.pipe(select($feature), tap(feature => {
        console.log({ feature })
    }))

    constructor(private store: Store<{}>) { }
}
  • reducers/app-initializer/index.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { produce } from 'immer';
import { µAppInitializerEntered } from '../../actions';

export interface AppInitializer {
    status: 'initial' | 'entered';
}

export const appInitializer = createReducer(
    {
        status: 'initial' as AppInitializer['status']
    },
    on(µAppInitializerEntered, (state): AppInitializer => produce(state, (draft) => {
        draft.status = 'entered';
    }))
);
  • reducers/index.ts
import { InjectionToken } from '@angular/core';
import { Action, ActionReducerMap, MetaReducer } from '@ngrx/store';
import { appInitializer, AppInitializer } from './app-initializer/index.reducer';

export interface FeatureState {
    appInitializer: AppInitializer;
}

export interface State {
    frontend: FeatureState;
}

export const reducers = new InjectionToken<ActionReducerMap<FeatureState, Action>>('frontend', {
    factory: () => ({ appInitializer })
});
  • selectors/index.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { FeatureState, State } from '../reducers';

export const $feature = createFeatureSelector<State, FeatureState>('frontend');
export const $appInitializer = createSelector($feature, (feature) => feature?.appInitializer);
export const $appInitializerEntered = createSelector($appInitializer, (appInitializer) => appInitializer?.status);
  • index.module.ts
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Store, StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { µAppInitializerEntered } from './actions';
import { IndexComponent } from './components/index.component';
import { reducers } from './reducers';

@NgModule({
    bootstrap: [IndexComponent],
    declarations: [IndexComponent],
    imports: [
        BrowserModule,
        StoreModule.forRoot(reducers, {
            runtimeChecks: {
                strictActionImmutability: true,
                strictActionSerializability: true,
                strictStateImmutability: true,
                strictStateSerializability: true
            }
        }),
        StoreDevtoolsModule.instrument({
            maxAge: 5000,
            name: 'frontend'
        })
    ],
    providers: [
        {
            provide: APP_INITIALIZER,
            useFactory: (store: Store<{}>) => () => store.dispatch(µAppInitializerEntered()),
            multi: true,
            deps: [Store]
        }
    ]
})
export class AppModule {
}

Context

  • package.json#dependencies
    "@angular/animations": "^10.1.0",
    "@angular/common": "^10.1.0",
    "@angular/compiler": "^10.1.0",
    "@angular/core": "^10.1.0",
    "@angular/forms": "^10.1.0",
    "@angular/platform-browser": "^10.1.0",
    "@angular/platform-browser-dynamic": "^10.1.0",
    "@angular/platform-server": "^10.1.0",
    "@angular/router": "^10.1.0",
    "@ngrx/effects": "^10.0.1",
    "@ngrx/router-store": "^10.0.1",
    "@ngrx/store": "^10.0.1",
    "@nrwl/node": "^10.4.4",
    "angular-oauth2-oidc": "^10.0.3",
    "immer": "^8.0.0",
    "lodash.random": "^3.2.0",
    "rxjs": "~6.5.5",
    "tslib": "^2.0.0",
    "zone.js": "^0.10.2"
  • yarn run nx report
yarn run v1.21.1
$ nx report

>  NX  Report complete - copy this into the issue template

  nx : Not Found
  @nrwl/angular : 10.4.4
  @nrwl/cli : 10.4.4
  @nrwl/cypress : 10.4.4
  @nrwl/eslint-plugin-nx : 10.4.4
  @nrwl/express : Not Found
  @nrwl/jest : 10.4.4
  @nrwl/linter : 10.4.4
  @nrwl/nest : Not Found
  @nrwl/next : Not Found
  @nrwl/node : 10.4.4
  @nrwl/react : Not Found
  @nrwl/schematics : Not Found
  @nrwl/tao : 10.4.4
  @nrwl/web : Not Found
  @nrwl/workspace : 10.4.4
  typescript : 4.0.5

Done in 1.68s.

Notes

I originally posted this issue on @ngrx/platform repo (here) rather than in here in StackOverflow because I tried to follow the NgRx documentation very carefully to do this MRE so I thought that maybe I missed a documentation somewhere which we can bring more visibility on in the official documentation.


Solution

  • As Tim Deschryver said here, the state is structured as:

    { appInitializer: { ... } }
    

    You have to change the factory for the root reducer or select the appInitizalizer leaf in the feature selector

    export const reducers = new InjectionToken<ActionReducerMap<any, Action>>(
      'frontend',
      {
    //                         👇 use the `frontend` property here, otherwise it will be   `appInitializer`
        factory: () => ({ frontend: appInitializer }),
      }
    );
    
    // or
    
    export const $feature = createFeatureSelector<State, AppInitializer>('appInitializer ');