Hadrien TOMA
Hadrien TOMA

Reputation: 2723

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:

import { createAction } from '@ngrx/store';

export const µAppInitializerEntered = createAction(`[frontend] µAppInitializerEntered`);
<pre>{{ feature$ | async | json }}</pre>
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<{}>) { }
}
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';
    }))
);
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 })
});
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);
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

    "@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 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.

Upvotes: 2

Views: 537

Answers (1)

Jad
Jad

Reputation: 108

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 ');

Upvotes: 2

Related Questions