Skip to content

Commit

Permalink
feat(Store): Allow initial state function for AoT compatibility (#59)
Browse files Browse the repository at this point in the history
resolves #51
  • Loading branch information
bfricka authored and brandonroberts committed Jul 9, 2017
1 parent b90df34 commit 1a166ec
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 40 deletions.
25 changes: 24 additions & 1 deletion docs/store/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -20,6 +20,29 @@ 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.
export function getInitialState() {
return {...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`
Expand Down
68 changes: 38 additions & 30 deletions modules/store/spec/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,51 @@ interface TodoAppSchema {
todos: Todo[];
}



describe('ngRx Store', () => {
let injector: ReflectiveInjector;
let store: Store<TestAppSchema>;
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<TestAppSchema>;
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();
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 6 additions & 2 deletions modules/store/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export interface Action {
type: string;
}

export type TypeId<T> = () => T;

export type InitialState<T> = Partial<T> | TypeId<Partial<T>> | void;

export interface ActionReducer<T, V extends Action = Action> {
(state: T | undefined, action: V): T;
}
Expand All @@ -11,14 +15,14 @@ export type ActionReducerMap<T, V extends Action = Action> = {
};

export interface ActionReducerFactory<T, V extends Action = Action> {
(reducerMap: ActionReducerMap<T, V>, initialState?: Partial<T>): ActionReducer<T, V>;
(reducerMap: ActionReducerMap<T, V>, initialState?: InitialState<T>): ActionReducer<T, V>;
}

export interface StoreFeature<T, V extends Action = Action> {
key: string;
reducers: ActionReducerMap<T, V> | ActionReducer<T, V>;
reducerFactory: ActionReducerFactory<T, V>;
initialState: T | undefined;
initialState?: InitialState<T>;
}

export interface Selector<T, V> {
Expand Down
20 changes: 14 additions & 6 deletions modules/store/src/store_module.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -29,7 +27,7 @@ export class StoreFeatureModule implements OnDestroy {
}
}

export type StoreConfig<T, V extends Action = Action> = { initialState?: T, reducerFactory?: ActionReducerFactory<T, V> };
export type StoreConfig<T, V extends Action = Action> = { initialState?: InitialState<T>, reducerFactory?: ActionReducerFactory<T, V> };

@NgModule({})
export class StoreModule {
Expand All @@ -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,
Expand Down Expand Up @@ -70,3 +69,12 @@ export class StoreModule {
};
}
}

/** @internal */
export function _initialStateFactory(initialState: any): any {
if (typeof initialState === 'function') {
return initialState();
}

return initialState;
}
3 changes: 2 additions & 1 deletion modules/store/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down

0 comments on commit 1a166ec

Please sign in to comment.