Skip to content

Commit 96e5175

Browse files
committed
test: add tests for performance optimizations and bail-outs
1 parent 618dfc7 commit 96e5175

File tree

3 files changed

+179
-1
lines changed

3 files changed

+179
-1
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
function is(x: unknown, y: unknown) {
2+
if (x === y) {
3+
return x !== 0 || y !== 0 || 1 / x === 1 / y
4+
} else {
5+
return x !== x && y !== y
6+
}
7+
}
8+
9+
export function shallowEqual(objA: any, objB: any) {
10+
if (is(objA, objB)) return true
11+
12+
if (
13+
typeof objA !== 'object' ||
14+
objA === null ||
15+
typeof objB !== 'object' ||
16+
objB === null
17+
) {
18+
return false
19+
}
20+
21+
const keysA = Object.keys(objA)
22+
const keysB = Object.keys(objB)
23+
24+
if (keysA.length !== keysB.length) return false
25+
26+
for (let i = 0; i < keysA.length; i++) {
27+
if (
28+
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
29+
!is(objA[keysA[i]], objB[keysA[i]])
30+
) {
31+
return false
32+
}
33+
}
34+
35+
return true
36+
}

projects/angular-redux/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './lib/inject-selector';
77
export * from './lib/inject-store';
88
export * from './lib/provide-redux';
99
export * from "./lib/provider"
10+
export * from "./lib/utils/shallowEqual"

projects/angular-redux/src/tests/inject-selector.spec.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {InjectSelector, injectSelector, provideRedux, ReduxProvider} from '../public-api'
1+
import {InjectSelector, injectSelector, provideRedux, ReduxProvider, shallowEqual} from '../public-api'
22
import {Component, effect, inject} from "@angular/core";
33
import {render, waitFor} from "@testing-library/angular";
44
import {AnyAction, createStore, Store} from "redux";
@@ -193,3 +193,144 @@ describe('injectSelector lifecycle interactions', () => {
193193
)
194194
})
195195
});
196+
197+
describe('performance optimizations and bail-outs', () => {
198+
it('defaults to ref-equality to prevent unnecessary updates', async () => {
199+
const state = {}
200+
const store = createStore(() => state)
201+
202+
@Component({
203+
selector: "app-root",
204+
standalone: true,
205+
template: "<div></div>"
206+
})
207+
class Comp {
208+
value = injectSelector((s) => s)
209+
_test = effect(() => {
210+
renderedItems.push(this.value())
211+
})
212+
}
213+
214+
await render(Comp, {
215+
providers: [provideRedux({store})]
216+
})
217+
218+
219+
expect(renderedItems.length).toBe(1)
220+
221+
store.dispatch({type: ''})
222+
223+
await waitFor(() =>
224+
expect(renderedItems.length).toBe(1)
225+
)
226+
})
227+
228+
it('allows other equality functions to prevent unnecessary updates', async () => {
229+
interface StateType {
230+
count: number
231+
stable: {}
232+
}
233+
234+
const store = createStore(
235+
({count, stable}: StateType = {count: -1, stable: {}}) => ({
236+
count: count + 1,
237+
stable,
238+
}),
239+
)
240+
241+
@Component({
242+
selector: "app-comp",
243+
standalone: true,
244+
template: "<div></div>"
245+
})
246+
class Comp {
247+
value = injectSelector(
248+
(s: StateType) => Object.keys(s),
249+
shallowEqual,
250+
)
251+
_test = effect(() => {
252+
renderedItems.push(this.value())
253+
})
254+
}
255+
256+
@Component({
257+
selector: "app-other",
258+
standalone: true,
259+
template: "<div></div>"
260+
})
261+
class Comp2 {
262+
value = injectSelector((s: StateType) => Object.keys(s), {
263+
equalityFn: shallowEqual,
264+
})
265+
_test = effect(() => {
266+
renderedItems.push(this.value())
267+
})
268+
}
269+
270+
@Component({
271+
selector: "app-root",
272+
standalone: true,
273+
imports: [Comp, Comp2],
274+
template: `
275+
<app-comp/>
276+
<app-other/>
277+
`
278+
})
279+
class App {
280+
}
281+
282+
await render(App, {
283+
providers: [provideRedux({store})]
284+
})
285+
286+
expect(renderedItems.length).toBe(2)
287+
288+
store.dispatch({type: ''})
289+
290+
await waitFor(() =>
291+
expect(renderedItems.length).toBe(2)
292+
)
293+
});
294+
295+
it('calls selector exactly once on mount and on update', async () => {
296+
interface StateType {
297+
count: number
298+
}
299+
300+
const store = createStore(({count}: StateType = {count: 0}) => ({
301+
count: count + 1,
302+
}))
303+
304+
const selector = jest.fn((s: StateType) => {
305+
return s.count
306+
})
307+
const renderedItems: number[] = []
308+
309+
310+
@Component({
311+
selector: "app-root",
312+
standalone: true,
313+
template: "<div></div>"
314+
})
315+
class Comp {
316+
value = injectSelector(selector)
317+
_test = effect(() => {
318+
renderedItems.push(this.value())
319+
})
320+
}
321+
322+
await render(Comp, {
323+
providers: [provideRedux({store})]
324+
})
325+
326+
expect(selector).toHaveBeenCalledTimes(1)
327+
expect(renderedItems.length).toEqual(1)
328+
329+
store.dispatch({type: ''})
330+
331+
await waitFor(() =>
332+
expect(selector).toHaveBeenCalledTimes(2)
333+
)
334+
expect(renderedItems.length).toEqual(2)
335+
});
336+
});

0 commit comments

Comments
 (0)