ngRx/store Observable value not showing up in Angular template

1.5k views Asked by At

I am using a pretty contrived set of examples from the ngRx docs to try and get started with a redux model for our Angular app. The code below works--all the actions fire and update the store correctly. I can see them in the redux dev tools and the store logger.

However, I am unable to get anything to show up in the template. It's just blank. I am unsure if it's related to the three layers of the state tree which looks like this:

grandpa: {
  grandpa: {
    grandpaCounter: 50
  }
}

I have tried to follow the ngRx example-app with my use of reselect, but perhaps I'm misusing those selectors? What else could I be missing?

app.component.html

<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<div>Current Count: {{ grandpaCounter$ | async }}</div>

<button (click)="resetCounter()">Reset Counter</button>

app.module.ts

// Native angular modules
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' /* Registers critical application service providers */
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { HttpClientModule } from '@angular/common/http'

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

// ngRx
import { StoreModule } from '@ngrx/store'
import { StoreDevtoolsModule } from '@ngrx/store-devtools'
import { EffectsModule } from '@ngrx/effects'
import {
  StoreRouterConnectingModule,
  routerReducer as router
} from '@ngrx/router-store'

// Router
import { AppRoutingModule } from './app.routing'

// Shared module
import { SharedModule } from './shared/shared.module'

// Functional modules
import { GrandpaModule } from './modules/grandpa/grandpa.module'

// ngRx store middleware
import { metaReducers } from './app.store'

// Configuration
import { APP_CONFIG, AppConfig } from './app.config'

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    StoreModule.forRoot({ router: router }, { metaReducers }),
    StoreRouterConnectingModule,
    StoreDevtoolsModule.instrument({
      maxAge: 25 //  Retains last 25 states
    }),
    EffectsModule.forRoot([]),
    HttpClientModule,
    SharedModule,
    GrandpaModule,
    AppRoutingModule // must be last
  ],
  declarations: [AppComponent],
  providers: [{ provide: APP_CONFIG, useValue: AppConfig }],
  bootstrap: [AppComponent]
})
export class AppModule {}

app.store.ts

// ngRx
import { ActionReducer, MetaReducer } from '@ngrx/store'

import { storeLogger } from 'ngrx-store-logger'

// Root state
export interface State {}

/* Meta-reducers */
export function logger(reducer: ActionReducer<State>): any {
  // default, no options
  return storeLogger()(reducer)
}

export const metaReducers = process.env.ENV === 'production' ? [] : [logger]

grandpa.module.ts

// Native angular modules
import { NgModule } from '@angular/core'

// ngRx
import { StoreModule } from '@ngrx/store'

// Shared module
import { SharedModule } from '../../shared/shared.module'

// Functional Components
import { GrandpaComponent } from './grandpa.component'

// Router
import { GrandpaRoutingModule } from './grandpa.routing'

// Store
import { branchReducers } from './grandpa.store'

@NgModule({
  imports: [
    StoreModule.forFeature('grandpa', branchReducers),
    SharedModule,
    GrandpaRoutingModule // must be last
  ],
  declarations: [GrandpaComponent]
})
export class GrandpaModule {}

grandpa.store.ts

// ngRx
import { ActionReducerMap, createSelector } from '@ngrx/store'

// Module branch reducers
import * as grandpaReducer from './grandpa.reducer'

// Feature state
export interface State {
  grandpa: grandpaReducer.State
}

// Feature reducers map
export const branchReducers: ActionReducerMap<State> = {
  grandpa: grandpaReducer.reducer
}

// Module selectors
export const getGrandpaState = (state: State) => state.grandpa

export const getGrandpaCounter = createSelector(
  getGrandpaState,
  grandpaReducer.getGrandpaCounter
)

grandpa.reducer.ts

import { createSelector } from '@ngrx/store'

import * as GrandpaActions from './grandpa.actions'
import * as grandpaStore from './grandpa.store'

export interface State {
  grandpaCounter: number
}

export const initialState: State = {
  grandpaCounter: 50
}

export function reducer(state = initialState, action: GrandpaActions.Actions) {
  switch (action.type) {
    case GrandpaActions.INCREMENT:
      return { grandpaCounter: state.grandpaCounter + 1 }

    case GrandpaActions.DECREMENT:
      return { grandpaCounter: state.grandpaCounter - 1 }

    case GrandpaActions.RESET_COUNTER:
      return { grandpaCounter: initialState.grandpaCounter }

    default:
      return { grandpaCounter: state.grandpaCounter }
  }
}

// Selectors
export const getGrandpaCounter = (state: State) => state.grandpaCounter

grandpa.component.ts

import { Component } from '@angular/core'

import { Observable } from 'rxjs/Observable'

import { Store } from '@ngrx/store'

import * as GrandpaActions from './grandpa.actions'
import * as grandpaStore from './grandpa.store'

@Component({
  selector: 'portal-grandpa',
  templateUrl: './grandpa.component.html'
})
export class GrandpaComponent {
  grandpaCounter$: Observable<number>

  constructor(private store: Store<grandpaStore.State>) {
    this.grandpaCounter$ = store.select(grandpaStore.getGrandpaCounter)
  }
  increment() {
    this.store.dispatch(new GrandpaActions.Increment())
  }
  decrement() {
    this.store.dispatch(new GrandpaActions.Decrement())
  }
  resetCounter() {
    this.store.dispatch(new GrandpaActions.ResetCounter())
  }
}
1

There are 1 answers

0
indigo On BEST ANSWER

It ended up being the state tree and the selectors. I needed to define my selector slices out to the three levels of state:

grandpa.store.ts

// ngRx
import {
  ActionReducerMap,
  createSelector,
  createFeatureSelector
} from '@ngrx/store'

// Branch reducers
import * as grandpaReducer from './grandpa.reducer'

// State
export interface State {
  grandpa: grandpaReducer.State
}

// Reducers map
export const branchReducers: ActionReducerMap<State> = {
  grandpa: grandpaReducer.reducer
}

// Selectors
export const getS0 = createFeatureSelector<State>('grandpa')
export const getS1 = (state: State) => state.grandpa
export const getS01 = createSelector(getS0, getS1)
export const getS2 = createSelector(getS01, grandpaReducer.getGrandpaCounter)

grandpa.component.ts

import { Component } from '@angular/core'

import { Observable } from 'rxjs/Observable'

import { Store } from '@ngrx/store'

import * as GrandpaActions from './grandpa.actions'
import * as grandpaStore from './grandpa.store'

@Component({
  selector: 'portal-grandpa',
  templateUrl: './grandpa.component.html'
})
export class GrandpaComponent {
  grandpaCounter$: Observable<number>

  constructor(private store: Store<grandpaStore.State>) {
    this.grandpaCounter$ = store.select(grandpaStore.getS2)
  }
  increment() {
    this.store.dispatch(new GrandpaActions.Increment())
  }
  decrement() {
    this.store.dispatch(new GrandpaActions.Decrement())
  }
  resetCounter() {
    this.store.dispatch(new GrandpaActions.ResetCounter())
  }
}

I'm still not sure why attaching the reducer state and its direct selector does not work however. I would have thought this single selector would have worked with it:

grandpa.reducer.ts

export const getGrandpaCounter = (state: State) => state.grandpaCounter

grandpa.component.ts

import { Component } from '@angular/core'

import { Observable } from 'rxjs/Observable'

import { Store } from '@ngrx/store'

import * as GrandpaActions from './grandpa.actions'

import * as grandpaReducer from './grandpa.reducer'

@Component({
  selector: 'portal-grandpa',
  templateUrl: './grandpa.component.html'
})
export class GrandpaComponent {
  grandpaCounter$: Observable<number>

  constructor(private store: Store<grandpaReducer.State>) {
    this.grandpaCounter$ = store.select(grandpaReducer.getGrandpaCounter)
  }
  increment() {
    this.store.dispatch(new GrandpaActions.Increment())
  }
  decrement() {
    this.store.dispatch(new GrandpaActions.Decrement())
  }
  resetCounter() {
    this.store.dispatch(new GrandpaActions.ResetCounter())
  }
}