Skip to content

Commit 1a166ec

Browse files
bfrickabrandonroberts
authored andcommitted
feat(Store): Allow initial state function for AoT compatibility (#59)
resolves #51
1 parent b90df34 commit 1a166ec

File tree

5 files changed

+84
-40
lines changed

5 files changed

+84
-40
lines changed

docs/store/api.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Initial State
44

5-
Configure initial state when providing Store:
5+
Configure initial state when providing Store. `config.initialState` can be either the actual state, or a function that returns the initial state:
66

77
```ts
88
import { StoreModule } from '@ngrx/store';
@@ -20,6 +20,29 @@ import { reducers } from './reducers';
2020
export class AppModule {}
2121
```
2222

23+
### Initial State and Ahead of Time (AoT) Compilation
24+
25+
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:
26+
27+
```ts
28+
/// Pretend this is dynamically injected at runtime
29+
const initialStateFromSomewhere = { counter: 3 };
30+
31+
/// Static state
32+
const initialState = { counter: 2 };
33+
34+
/// In this function dynamic state slices, if they exist, will overwrite static state at runtime.
35+
export function getInitialState() {
36+
return {...initialState, ...initialStateFromSomewhere};
37+
}
38+
39+
@NgModule({
40+
imports: [
41+
StoreModule.forRoot(reducers, {initialState: getInitialState})
42+
]
43+
})
44+
```
45+
2346
## Reducer Factory
2447

2548
@ngrx/store composes your map of reducers into a single reducer. Use the `reducerFactory`

modules/store/spec/store.spec.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,51 @@ interface TodoAppSchema {
2121
todos: Todo[];
2222
}
2323

24-
25-
2624
describe('ngRx Store', () => {
25+
let injector: ReflectiveInjector;
26+
let store: Store<TestAppSchema>;
27+
let dispatcher: ActionsSubject;
28+
29+
function setup(initialState: any = { counter1: 0, counter2: 1 }) {
30+
const reducers = {
31+
counter1: counterReducer,
32+
counter2: counterReducer,
33+
counter3: counterReducer
34+
};
2735

28-
describe('basic store actions', function() {
29-
30-
let injector: ReflectiveInjector;
31-
let store: Store<TestAppSchema>;
32-
let dispatcher: ActionsSubject;
33-
let initialState: any;
36+
injector = createInjector(StoreModule.forRoot(reducers, { initialState }));
37+
store = injector.get(Store);
38+
dispatcher = injector.get(ActionsSubject);
39+
}
3440

35-
beforeEach(() => {
36-
const reducers = {
37-
counter1: counterReducer,
38-
counter2: counterReducer,
39-
counter3: counterReducer
40-
};
41+
describe('initial state', () => {
42+
it('should handle an initial state object', (done) => {
43+
setup();
4144

42-
initialState = { counter1: 0, counter2: 1 };
45+
store.take(1).subscribe({
46+
next(val) {
47+
expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 });
48+
},
49+
error: done,
50+
complete: done
51+
});
52+
});
4353

44-
injector = createInjector(StoreModule.forRoot(reducers, { initialState }));
54+
it('should handle an initial state function', (done) => {
55+
setup(() => ({ counter1: 0, counter2: 5 }));
4556

46-
store = injector.get(Store);
47-
dispatcher = injector.get(ActionsSubject);
57+
store.take(1).subscribe({
58+
next(val) {
59+
expect(val).toEqual({ counter1: 0, counter2: 5, counter3: 0 });
60+
},
61+
error: done,
62+
complete: done
63+
});
4864
});
65+
});
66+
67+
describe('basic store actions', function() {
68+
beforeEach(() => setup());
4969

5070
it('should provide an Observable Store', () => {
5171
expect(store).toBeDefined();
@@ -98,18 +118,6 @@ describe('ngRx Store', () => {
98118

99119
});
100120

101-
it('should appropriately handle initial state', (done) => {
102-
103-
store.take(1).subscribe({
104-
next(val) {
105-
expect(val).toEqual({ counter1: 0, counter2: 1, counter3: 0 });
106-
},
107-
error: done,
108-
complete: done
109-
});
110-
111-
});
112-
113121
it('should increment and decrement counter1', function() {
114122

115123
const counterSteps = hot(actionSequence, actionValues);

modules/store/src/models.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export interface Action {
22
type: string;
33
}
44

5+
export type TypeId<T> = () => T;
6+
7+
export type InitialState<T> = Partial<T> | TypeId<Partial<T>> | void;
8+
59
export interface ActionReducer<T, V extends Action = Action> {
610
(state: T | undefined, action: V): T;
711
}
@@ -11,14 +15,14 @@ export type ActionReducerMap<T, V extends Action = Action> = {
1115
};
1216

1317
export interface ActionReducerFactory<T, V extends Action = Action> {
14-
(reducerMap: ActionReducerMap<T, V>, initialState?: Partial<T>): ActionReducer<T, V>;
18+
(reducerMap: ActionReducerMap<T, V>, initialState?: InitialState<T>): ActionReducer<T, V>;
1519
}
1620

1721
export interface StoreFeature<T, V extends Action = Action> {
1822
key: string;
1923
reducers: ActionReducerMap<T, V> | ActionReducer<T, V>;
2024
reducerFactory: ActionReducerFactory<T, V>;
21-
initialState: T | undefined;
25+
initialState?: InitialState<T>;
2226
}
2327

2428
export interface Selector<T, V> {

modules/store/src/store_module.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { NgModule, Inject, ModuleWithProviders, OnDestroy, InjectionToken } from '@angular/core';
2-
import { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, StoreFeature } from './models';
2+
import { Action, ActionReducer, ActionReducerMap, ActionReducerFactory, StoreFeature, InitialState } from './models';
33
import { combineReducers } from './utils';
4-
import { INITIAL_STATE, INITIAL_REDUCERS, REDUCER_FACTORY, STORE_FEATURES } from './tokens';
4+
import { INITIAL_STATE, INITIAL_REDUCERS, REDUCER_FACTORY, STORE_FEATURES, _INITIAL_STATE } from './tokens';
55
import { ACTIONS_SUBJECT_PROVIDERS } from './actions_subject';
66
import { REDUCER_MANAGER_PROVIDERS, ReducerManager } from './reducer_manager';
77
import { SCANNED_ACTIONS_SUBJECT_PROVIDERS } from './scanned_actions_subject';
88
import { STATE_PROVIDERS } from './state';
99
import { STORE_PROVIDERS } from './store';
1010

11-
12-
1311
@NgModule({})
1412
export class StoreRootModule {
1513

@@ -29,7 +27,7 @@ export class StoreFeatureModule implements OnDestroy {
2927
}
3028
}
3129

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

3432
@NgModule({})
3533
export class StoreModule {
@@ -38,7 +36,8 @@ export class StoreModule {
3836
return {
3937
ngModule: StoreRootModule,
4038
providers: [
41-
{ provide: INITIAL_STATE, useValue: config.initialState },
39+
{ provide: _INITIAL_STATE, useValue: config.initialState },
40+
{ provide: INITIAL_STATE, useFactory: _initialStateFactory, deps: [ _INITIAL_STATE ] },
4241
reducers instanceof InjectionToken ? { provide: INITIAL_REDUCERS, useExisting: reducers } : { provide: INITIAL_REDUCERS, useValue: reducers },
4342
{ provide: REDUCER_FACTORY, useValue: config.reducerFactory ? config.reducerFactory : combineReducers },
4443
ACTIONS_SUBJECT_PROVIDERS,
@@ -70,3 +69,12 @@ export class StoreModule {
7069
};
7170
}
7271
}
72+
73+
/** @internal */
74+
export function _initialStateFactory(initialState: any): any {
75+
if (typeof initialState === 'function') {
76+
return initialState();
77+
}
78+
79+
return initialState;
80+
}

modules/store/src/tokens.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OpaqueToken } from '@angular/core';
22

3-
3+
/** @internal */
4+
export const _INITIAL_STATE = new OpaqueToken('_ngrx/store Initial State');
45
export const INITIAL_STATE = new OpaqueToken('@ngrx/store Initial State');
56
export const REDUCER_FACTORY = new OpaqueToken('@ngrx/store Reducer Factory');
67
export const INITIAL_REDUCERS = new OpaqueToken('@ngrx/store Initial Reducers');

0 commit comments

Comments
 (0)