Skip to content

Commit c63941c

Browse files
REPLicatedbrandonroberts
authored andcommitted
fix(store): improve consistency of memoized selector result when projection fails (#2101)
A memoized selector projection now consistently fails after the first evaluation, if it is evaluated again with the same state, instead of returning that last successful evaluation result from a previous state. Closes #2100
1 parent 70a8f2d commit c63941c

File tree

2 files changed

+58
-82
lines changed

2 files changed

+58
-82
lines changed

modules/store/spec/selector.spec.ts

Lines changed: 42 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,17 @@ describe('Selectors', () => {
4141
it('should deliver the value of selectors to the projection function', () => {
4242
const projectFn = jasmine.createSpy('projectionFn');
4343

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

5048
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
5149
});
5250

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

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

6256
expect(selector.projector()).toBe(2);
6357

@@ -70,11 +64,7 @@ describe('Selectors', () => {
7064

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

7969
selector.projector('', '');
8070

@@ -92,10 +82,7 @@ describe('Selectors', () => {
9282
return state.unchanged;
9383
});
9484
const projectFn = jasmine.createSpy('projectionFn');
95-
const selector = createSelector(
96-
neverChangingSelector,
97-
projectFn
98-
);
85+
const selector = createSelector(neverChangingSelector, projectFn);
9986

10087
selector(firstState);
10188
selector(secondState);
@@ -126,13 +113,33 @@ describe('Selectors', () => {
126113
expect(projectFn).toHaveBeenCalledTimes(2);
127114
});
128115

116+
it('should not memoize last successful projection result in case of error', () => {
117+
const firstState = { ok: true };
118+
const secondState = { ok: false };
119+
const fail = () => {
120+
throw new Error();
121+
};
122+
const projectorFn = jasmine
123+
.createSpy('projectorFn', (s: any) => (s.ok ? s.ok : fail()))
124+
.and.callThrough();
125+
const selectorFn = jasmine
126+
.createSpy('selectorFn', createSelector(state => state, projectorFn))
127+
.and.callThrough();
128+
129+
selectorFn(firstState);
130+
131+
expect(() => selectorFn(secondState)).toThrow(new Error());
132+
expect(() => selectorFn(secondState)).toThrow(new Error());
133+
134+
selectorFn(firstState);
135+
expect(selectorFn).toHaveBeenCalledTimes(4);
136+
expect(projectorFn).toHaveBeenCalledTimes(3);
137+
});
138+
129139
it('should allow you to release memoized arguments', () => {
130140
const state = { first: 'state' };
131141
const projectFn = jasmine.createSpy('projectionFn');
132-
const selector = createSelector(
133-
incrementOne,
134-
projectFn
135-
);
142+
const selector = createSelector(incrementOne, projectFn);
136143

137144
selector(state);
138145
selector(state);
@@ -144,18 +151,9 @@ describe('Selectors', () => {
144151
});
145152

146153
it('should recursively release ancestor selectors', () => {
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-
);
154+
const grandparent = createSelector(incrementOne, a => a);
155+
const parent = createSelector(grandparent, a => a);
156+
const child = createSelector(parent, a => a);
159157
spyOn(grandparent, 'release').and.callThrough();
160158
spyOn(parent, 'release').and.callThrough();
161159

@@ -271,20 +269,16 @@ describe('Selectors', () => {
271269
describe('createSelector with arrays', () => {
272270
it('should deliver the value of selectors to the projection function', () => {
273271
const projectFn = jasmine.createSpy('projectionFn');
274-
const selector = createSelector(
275-
[incrementOne, incrementTwo],
276-
projectFn
277-
)({});
272+
const selector = createSelector([incrementOne, incrementTwo], projectFn)(
273+
{}
274+
);
278275

279276
expect(projectFn).toHaveBeenCalledWith(countOne, countTwo);
280277
});
281278

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

289283
selector.projector('', '');
290284

@@ -302,10 +296,7 @@ describe('Selectors', () => {
302296
return state.unchanged;
303297
});
304298
const projectFn = jasmine.createSpy('projectionFn');
305-
const selector = createSelector(
306-
[neverChangingSelector],
307-
projectFn
308-
);
299+
const selector = createSelector([neverChangingSelector], projectFn);
309300

310301
selector(firstState);
311302
selector(secondState);
@@ -337,10 +328,7 @@ describe('Selectors', () => {
337328
it('should allow you to release memoized arguments', () => {
338329
const state = { first: 'state' };
339330
const projectFn = jasmine.createSpy('projectionFn');
340-
const selector = createSelector(
341-
[incrementOne],
342-
projectFn
343-
);
331+
const selector = createSelector([incrementOne], projectFn);
344332

345333
selector(state);
346334
selector(state);
@@ -352,18 +340,9 @@ describe('Selectors', () => {
352340
});
353341

354342
it('should recursively release ancestor selectors', () => {
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-
);
343+
const grandparent = createSelector([incrementOne], a => a);
344+
const parent = createSelector([grandparent], a => a);
345+
const child = createSelector([parent], a => a);
367346
spyOn(grandparent, 'release').and.callThrough();
368347
spyOn(parent, 'release').and.callThrough();
369348

modules/store/src/selector.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ export function defaultMemoize(
9595
return lastResult;
9696
}
9797

98+
const newResult = projectionFn.apply(null, arguments as any);
9899
lastArguments = arguments;
99100

100-
const newResult = projectionFn.apply(null, arguments as any);
101101
if (isResultEqual(lastResult, newResult)) {
102102
return lastResult;
103103
}
@@ -603,22 +603,19 @@ export function createFeatureSelector<T, V>(
603603
export function createFeatureSelector(
604604
featureName: any
605605
): MemoizedSelector<any, any> {
606-
return createSelector(
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-
},
622-
(featureState: any) => featureState
623-
);
606+
return createSelector((state: any) => {
607+
const featureState = state[featureName];
608+
if (isDevMode() && featureState === undefined) {
609+
console.warn(
610+
`The feature name \"${featureName}\" does ` +
611+
'not exist in the state, therefore createFeatureSelector ' +
612+
'cannot access it. Be sure it is imported in a loaded module ' +
613+
`using StoreModule.forRoot('${featureName}', ...) or ` +
614+
`StoreModule.forFeature('${featureName}', ...). If the default ` +
615+
'state is intended to be undefined, as is the case with router ' +
616+
'state, this development-only warning message can be ignored.'
617+
);
618+
}
619+
return featureState;
620+
}, (featureState: any) => featureState);
624621
}

0 commit comments

Comments
 (0)