From 2087926bdfefbcaac8ec3dfabc098088d9ed7851 Mon Sep 17 00:00:00 2001 From: Brian Frichette Date: Mon, 3 Jul 2017 22:56:10 -0700 Subject: [PATCH] Allow initial state function for AoT compatibility resolves #51 --- docs/store/api.md | 23 ++++++++++- modules/store/spec/store.spec.ts | 68 +++++++++++++++++-------------- modules/store/src/models.ts | 8 +++- modules/store/src/store_module.ts | 20 ++++++--- modules/store/src/tokens.ts | 3 +- 5 files changed, 82 insertions(+), 40 deletions(-) diff --git a/docs/store/api.md b/docs/store/api.md index 3c003913da..a08dc0d016 100644 --- a/docs/store/api.md +++ b/docs/store/api.md @@ -2,7 +2,7 @@ ## Initial State -Configure initial state when providing Store: +Configure initial state when providing Store. `config.initialState` can be either the actual state, or a function that returns the initial state: ```ts import { StoreModule } from '@ngrx/store'; @@ -20,6 +20,27 @@ import { reducers } from './reducers'; export class AppModule {} ``` +### Initial State and Ahead of Time (AoT) Compilation + +Angular AoT requires all symbols referenced in the construction of its types (think `@NgModule`, `@Component`, `@Injectable`, etc.) to be statically defined. For this reason, we cannot dynamically inject state at runtime with AoT unless we provide `initialState` as a function. Thus the above `NgModule` definition simply changes to: + +```ts +/// Pretend this is dynamically injected at runtime +const initialStateFromSomewhere = { counter: 3 }; + +/// Static state +const initialState = { counter: 2 }; + +/// In this function dynamic state slices, if they exist, will overwrite static state at runtime. +const getInitialState = () => ({...initialState, ...initialStateFromSomewhere}); + +@NgModule({ + imports: [ + StoreModule.forRoot(reducers, {initialState: getInitialState}) + ] +}) +``` + ## Reducer Factory @ngrx/store composes your map of reducers into a single reducer. Use the `reducerFactory` diff --git a/modules/store/spec/store.spec.ts b/modules/store/spec/store.spec.ts index 3d997aee0c..a807915ee9 100644 --- a/modules/store/spec/store.spec.ts +++ b/modules/store/spec/store.spec.ts @@ -21,31 +21,51 @@ interface TodoAppSchema { todos: Todo[]; } - - describe('ngRx Store', () => { + let injector: ReflectiveInjector; + let store: Store; + let dispatcher: ActionsSubject; + + function setup(initialState: any = { counter1: 0, counter2: 1 }) { + const reducers = { + counter1: counterReducer, + counter2: counterReducer, + counter3: counterReducer + }; - describe('basic store actions', function() { - - let injector: ReflectiveInjector; - let store: Store; - let dispatcher: ActionsSubject; - let initialState: any; + injector = createInjector(StoreModule.forRoot(reducers, { initialState })); + store = injector.get(Store); + dispatcher = injector.get(ActionsSubject); + } - beforeEach(() => { - const reducers = { - counter1: counterReducer, - counter2: counterReducer, - counter3: counterReducer - }; + describe('initial state', () => { + it('should handle an initial state object', (done) => { + setup(); - initialState = { counter1: 0, counter2: 1 }; + store.take(1).subscribe({ + next(val) { + expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 }); + }, + error: done, + complete: done + }); + }); - injector = createInjector(StoreModule.forRoot(reducers, { initialState })); + it('should handle an initial state function', (done) => { + setup(() => ({ counter1: 0, counter2: 5 })); - store = injector.get(Store); - dispatcher = injector.get(ActionsSubject); + store.take(1).subscribe({ + next(val) { + expect(val).toEqual({ counter1: 0, counter2: 5, counter3: 0 }); + }, + error: done, + complete: done + }); }); + }); + + describe('basic store actions', function() { + beforeEach(() => setup()); it('should provide an Observable Store', () => { expect(store).toBeDefined(); @@ -98,18 +118,6 @@ describe('ngRx Store', () => { }); - it('should appropriately handle initial state', (done) => { - - store.take(1).subscribe({ - next(val) { - expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 }); - }, - error: done, - complete: done - }); - - }); - it('should increment and decrement counter1', function() { const counterSteps = hot(actionSequence, actionValues); diff --git a/modules/store/src/models.ts b/modules/store/src/models.ts index e79fec2654..1bf2067dbd 100644 --- a/modules/store/src/models.ts +++ b/modules/store/src/models.ts @@ -2,6 +2,10 @@ export interface Action { type: string; } +export type TypeId = () => T; + +export type InitialState = Partial | TypeId> | void; + export interface ActionReducer { (state: T | undefined, action: V): T; } @@ -11,14 +15,14 @@ export type ActionReducerMap = { }; export interface ActionReducerFactory { - (reducerMap: ActionReducerMap, initialState?: Partial): ActionReducer; + (reducerMap: ActionReducerMap, initialState?: InitialState): ActionReducer; } export interface StoreFeature { key: string; reducers: ActionReducerMap | ActionReducer; reducerFactory: ActionReducerFactory; - initialState: T | undefined; + initialState?: InitialState; } export interface Selector { diff --git a/modules/store/src/store_module.ts b/modules/store/src/store_module.ts index a4ff94f115..f86eb79205 100644 --- a/modules/store/src/store_module.ts +++ b/modules/store/src/store_module.ts @@ -1,15 +1,13 @@ import { NgModule, Inject, ModuleWithProviders, OnDestroy, InjectionToken } from '@angular/core'; -import { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, StoreFeature } from './models'; +import { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, StoreFeature, InitialState } from './models'; import { combineReducers } from './utils'; -import { INITIAL_STATE, INITIAL_REDUCERS, REDUCER_FACTORY, STORE_FEATURES } from './tokens'; +import { INITIAL_STATE, INITIAL_REDUCERS, REDUCER_FACTORY, STORE_FEATURES, _INITIAL_STATE } from './tokens'; import { ACTIONS_SUBJECT_PROVIDERS } from './actions_subject'; import { REDUCER_MANAGER_PROVIDERS, ReducerManager } from './reducer_manager'; import { SCANNED_ACTIONS_SUBJECT_PROVIDERS } from './scanned_actions_subject'; import { STATE_PROVIDERS } from './state'; import { STORE_PROVIDERS } from './store'; - - @NgModule({}) export class StoreRootModule { @@ -29,7 +27,7 @@ export class StoreFeatureModule implements OnDestroy { } } -export type StoreConfig = { initialState?: T, reducerFactory?: ActionReducerFactory }; +export type StoreConfig = { initialState?: InitialState, reducerFactory?: ActionReducerFactory }; @NgModule({}) export class StoreModule { @@ -38,7 +36,8 @@ export class StoreModule { return { ngModule: StoreRootModule, providers: [ - { provide: INITIAL_STATE, useValue: config.initialState }, + { provide: _INITIAL_STATE, useValue: config.initialState }, + { provide: INITIAL_STATE, useFactory: _initialStateFactory, deps: [ _INITIAL_STATE ] }, reducers instanceof InjectionToken ? { provide: INITIAL_REDUCERS, useExisting: reducers } : { provide: INITIAL_REDUCERS, useValue: reducers }, { provide: REDUCER_FACTORY, useValue: config.reducerFactory ? config.reducerFactory : combineReducers }, ACTIONS_SUBJECT_PROVIDERS, @@ -70,3 +69,12 @@ export class StoreModule { }; } } + +/** @internal */ +export function _initialStateFactory(initialState: any): any { + if (typeof initialState === 'function') { + return initialState(); + } + + return initialState; +} diff --git a/modules/store/src/tokens.ts b/modules/store/src/tokens.ts index 2f1099beb5..c92203d1f4 100644 --- a/modules/store/src/tokens.ts +++ b/modules/store/src/tokens.ts @@ -1,6 +1,7 @@ import { OpaqueToken } from '@angular/core'; - +/** @internal */ +export const _INITIAL_STATE = new OpaqueToken('_ngrx/store Initial State'); export const INITIAL_STATE = new OpaqueToken('@ngrx/store Initial State'); export const REDUCER_FACTORY = new OpaqueToken('@ngrx/store Reducer Factory'); export const INITIAL_REDUCERS = new OpaqueToken('@ngrx/store Initial Reducers');