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())
}
}
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
grandpa.component.ts
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