Skip to content

Commit 6946e2e

Browse files
John Crowsonbrandonroberts
authored andcommitted
feat(store): add verbose error message for undefined feature state in development mode (#2078)
Closes #1897
1 parent eb5dbb9 commit 6946e2e

File tree

2 files changed

+127
-20
lines changed

2 files changed

+127
-20
lines changed

modules/store/spec/selector.spec.ts

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as ngCore from '@angular/core';
12
import { cold } from 'jasmine-marbles';
23
import {
34
createSelector,
@@ -40,17 +41,23 @@ describe('Selectors', () => {
4041
it('should deliver the value of selectors to the projection function', () => {
4142
const projectFn = jasmine.createSpy('projectionFn');
4243

43-
const selector = createSelector(incrementOne, incrementTwo, projectFn)(
44-
{}
45-
);
44+
const selector = createSelector(
45+
incrementOne,
46+
incrementTwo,
47+
projectFn
48+
)({});
4649

4750
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
4851
});
4952

5053
it('should allow an override of the selector return', () => {
5154
const projectFn = jasmine.createSpy('projectionFn').and.returnValue(2);
5255

53-
const selector = createSelector(incrementOne, incrementTwo, projectFn);
56+
const selector = createSelector(
57+
incrementOne,
58+
incrementTwo,
59+
projectFn
60+
);
5461

5562
expect(selector.projector()).toBe(2);
5663

@@ -63,7 +70,11 @@ describe('Selectors', () => {
6370

6471
it('should be possible to test a projector fn independent from the selectors it is composed of', () => {
6572
const projectFn = jasmine.createSpy('projectionFn');
66-
const selector = createSelector(incrementOne, incrementTwo, projectFn);
73+
const selector = createSelector(
74+
incrementOne,
75+
incrementTwo,
76+
projectFn
77+
);
6778

6879
selector.projector('', '');
6980

@@ -81,7 +92,10 @@ describe('Selectors', () => {
8192
return state.unchanged;
8293
});
8394
const projectFn = jasmine.createSpy('projectionFn');
84-
const selector = createSelector(neverChangingSelector, projectFn);
95+
const selector = createSelector(
96+
neverChangingSelector,
97+
projectFn
98+
);
8599

86100
selector(firstState);
87101
selector(secondState);
@@ -115,7 +129,10 @@ describe('Selectors', () => {
115129
it('should allow you to release memoized arguments', () => {
116130
const state = { first: 'state' };
117131
const projectFn = jasmine.createSpy('projectionFn');
118-
const selector = createSelector(incrementOne, projectFn);
132+
const selector = createSelector(
133+
incrementOne,
134+
projectFn
135+
);
119136

120137
selector(state);
121138
selector(state);
@@ -127,9 +144,18 @@ describe('Selectors', () => {
127144
});
128145

129146
it('should recursively release ancestor selectors', () => {
130-
const grandparent = createSelector(incrementOne, a => a);
131-
const parent = createSelector(grandparent, a => a);
132-
const child = createSelector(parent, a => a);
147+
const grandparent = createSelector(
148+
incrementOne,
149+
a => a
150+
);
151+
const parent = createSelector(
152+
grandparent,
153+
a => a
154+
);
155+
const child = createSelector(
156+
parent,
157+
a => a
158+
);
133159
spyOn(grandparent, 'release').and.callThrough();
134160
spyOn(parent, 'release').and.callThrough();
135161

@@ -245,16 +271,20 @@ describe('Selectors', () => {
245271
describe('createSelector with arrays', () => {
246272
it('should deliver the value of selectors to the projection function', () => {
247273
const projectFn = jasmine.createSpy('projectionFn');
248-
const selector = createSelector([incrementOne, incrementTwo], projectFn)(
249-
{}
250-
);
274+
const selector = createSelector(
275+
[incrementOne, incrementTwo],
276+
projectFn
277+
)({});
251278

252279
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
253280
});
254281

255282
it('should be possible to test a projector fn independent from the selectors it is composed of', () => {
256283
const projectFn = jasmine.createSpy('projectionFn');
257-
const selector = createSelector([incrementOne, incrementTwo], projectFn);
284+
const selector = createSelector(
285+
[incrementOne, incrementTwo],
286+
projectFn
287+
);
258288

259289
selector.projector('', '');
260290

@@ -272,7 +302,10 @@ describe('Selectors', () => {
272302
return state.unchanged;
273303
});
274304
const projectFn = jasmine.createSpy('projectionFn');
275-
const selector = createSelector([neverChangingSelector], projectFn);
305+
const selector = createSelector(
306+
[neverChangingSelector],
307+
projectFn
308+
);
276309

277310
selector(firstState);
278311
selector(secondState);
@@ -304,7 +337,10 @@ describe('Selectors', () => {
304337
it('should allow you to release memoized arguments', () => {
305338
const state = { first: 'state' };
306339
const projectFn = jasmine.createSpy('projectionFn');
307-
const selector = createSelector([incrementOne], projectFn);
340+
const selector = createSelector(
341+
[incrementOne],
342+
projectFn
343+
);
308344

309345
selector(state);
310346
selector(state);
@@ -316,9 +352,18 @@ describe('Selectors', () => {
316352
});
317353

318354
it('should recursively release ancestor selectors', () => {
319-
const grandparent = createSelector([incrementOne], a => a);
320-
const parent = createSelector([grandparent], a => a);
321-
const child = createSelector([parent], a => a);
355+
const grandparent = createSelector(
356+
[incrementOne],
357+
a => a
358+
);
359+
const parent = createSelector(
360+
[grandparent],
361+
a => a
362+
);
363+
const child = createSelector(
364+
[parent],
365+
a => a
366+
);
322367
spyOn(grandparent, 'release').and.callThrough();
323368
spyOn(parent, 'release').and.callThrough();
324369

@@ -433,9 +478,11 @@ describe('Selectors', () => {
433478
describe('createFeatureSelector', () => {
434479
let featureName = '@ngrx/router-store';
435480
let featureSelector: (state: any) => number;
481+
let warnSpy: jasmine.Spy;
436482

437483
beforeEach(() => {
438484
featureSelector = createFeatureSelector<number>(featureName);
485+
warnSpy = spyOn(console, 'warn');
439486
});
440487

441488
it('should memoize the result', () => {
@@ -455,6 +502,50 @@ describe('Selectors', () => {
455502
);
456503

457504
expect(featureState$).toBeObservable(expected$);
505+
expect(warnSpy).not.toHaveBeenCalled();
506+
});
507+
508+
it('should warn if the feature does not exist in the state', () => {
509+
spyOn(ngCore, 'isDevMode').and.returnValue(true);
510+
511+
const state = { otherState: '' };
512+
513+
const state$ = cold('a', { a: state });
514+
const expected$ = cold('a', { a: undefined });
515+
516+
const featureState$ = state$.pipe(
517+
map(featureSelector),
518+
distinctUntilChanged()
519+
);
520+
521+
expect(featureState$).toBeObservable(expected$);
522+
expect(warnSpy).toHaveBeenCalledWith(
523+
'The feature name "@ngrx/router-store" does not exist ' +
524+
'in the state, therefore createFeatureSelector cannot ' +
525+
'access it. Be sure it is imported in a loaded module using ' +
526+
"StoreModule.forRoot('@ngrx/router-store', ...) or " +
527+
"StoreModule.forFeature('@ngrx/router-store', ...). If the " +
528+
'default state is intended to be undefined, as is the case ' +
529+
'with router state, this development-only warning message can ' +
530+
'be ignored.'
531+
);
532+
});
533+
534+
it('should not warn if not in development mode', () => {
535+
spyOn(ngCore, 'isDevMode').and.returnValue(false);
536+
537+
const state = { otherState: '' };
538+
539+
const state$ = cold('a', { a: state });
540+
const expected$ = cold('a', { a: undefined });
541+
542+
const featureState$ = state$.pipe(
543+
map(featureSelector),
544+
distinctUntilChanged()
545+
);
546+
547+
expect(featureState$).toBeObservable(expected$);
548+
expect(warnSpy).not.toHaveBeenCalled();
458549
});
459550
});
460551

modules/store/src/selector.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Selector, SelectorWithProps } from './models';
2+
import { isDevMode } from '@angular/core';
23

34
export type AnyFn = (...args: any[]) => any;
45

@@ -274,6 +275,7 @@ export function createSelector<State, S1, S2, S3, S4, S5, S6, Result>(
274275
Selector<State, S1>,
275276
Selector<State, S2>,
276277
Selector<State, S3>,
278+
277279
Selector<State, S4>,
278280
Selector<State, S5>,
279281
Selector<State, S6>
@@ -602,7 +604,21 @@ export function createFeatureSelector(
602604
featureName: any
603605
): MemoizedSelector<any, any> {
604606
return createSelector(
605-
(state: any) => state[featureName],
607+
(state: any) => {
608+
const featureState = state[featureName];
609+
if (isDevMode() && featureState === undefined) {
610+
console.warn(
611+
`The feature name \"${featureName}\" does ` +
612+
'not exist in the state, therefore createFeatureSelector ' +
613+
'cannot access it. Be sure it is imported in a loaded module ' +
614+
`using StoreModule.forRoot('${featureName}', ...) or ` +
615+
`StoreModule.forFeature('${featureName}', ...). If the default ` +
616+
'state is intended to be undefined, as is the case with router ' +
617+
'state, this development-only warning message can be ignored.'
618+
);
619+
}
620+
return featureState;
621+
},
606622
(featureState: any) => featureState
607623
);
608624
}

0 commit comments

Comments
 (0)