Skip to content

Commit b820017

Browse files
committed
feat: add shallowEqual export
1 parent 2328188 commit b820017

File tree

7 files changed

+274
-15
lines changed

7 files changed

+274
-15
lines changed

packages/vue-redux/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export * from './compositions/use-dispatch'
55
export * from './compositions/use-selector'
66
export * from './compositions/use-redux-context'
77
export * from './types'
8+
export * from './utils/shallowEqual'
9+
export type { Subscription } from './utils/Subscription'

packages/vue-redux/src/utils/Subscription.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { defaultNoopBatch as batch } from './batch'
2-
31
// encapsulates the subscription logic for connecting a component to the redux store, as
42
// well as nesting subscriptions of descendant components, so that we can ensure the
53
// ancestor components re-render before descendants
@@ -23,13 +21,11 @@ function createListenerCollection() {
2321
},
2422

2523
notify() {
26-
batch(() => {
27-
let listener = first
28-
while (listener) {
29-
listener.callback()
30-
listener = listener.next
31-
}
32-
})
24+
let listener = first
25+
while (listener) {
26+
listener.callback()
27+
listener = listener.next
28+
}
3329
},
3430

3531
get() {

packages/vue-redux/src/utils/batch.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.
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 as Function).call(objB, keysA[i]) ||
29+
!is(objA[keysA[i]!], objB[keysA[i]!])
30+
) {
31+
return false
32+
}
33+
}
34+
35+
return true
36+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { createSubscription } from '../src/utils/Subscription'
3+
import type { Subscription } from '../src/utils/Subscription'
4+
import type { Store } from 'redux'
5+
6+
describe('Subscription', () => {
7+
let notifications: string[]
8+
let store: Store
9+
let parent: Subscription
10+
11+
beforeEach(() => {
12+
notifications = []
13+
store = { subscribe: () => vi.fn() } as unknown as Store
14+
15+
parent = createSubscription(store)
16+
parent.onStateChange = () => {}
17+
parent.trySubscribe()
18+
})
19+
20+
function subscribeChild(name: string) {
21+
const child = createSubscription(store, parent)
22+
child.onStateChange = () => notifications.push(name)
23+
child.trySubscribe()
24+
return child
25+
}
26+
27+
it('listeners are notified in order', () => {
28+
subscribeChild('child1')
29+
subscribeChild('child2')
30+
subscribeChild('child3')
31+
subscribeChild('child4')
32+
33+
parent.notifyNestedSubs()
34+
35+
expect(notifications).toEqual(['child1', 'child2', 'child3', 'child4'])
36+
})
37+
38+
it('listeners can be unsubscribed', () => {
39+
const child1 = subscribeChild('child1')
40+
const child2 = subscribeChild('child2')
41+
const child3 = subscribeChild('child3')
42+
43+
child2.tryUnsubscribe()
44+
parent.notifyNestedSubs()
45+
46+
expect(notifications).toEqual(['child1', 'child3'])
47+
notifications.length = 0
48+
49+
child1.tryUnsubscribe()
50+
parent.notifyNestedSubs()
51+
52+
expect(notifications).toEqual(['child3'])
53+
notifications.length = 0
54+
55+
child3.tryUnsubscribe()
56+
parent.notifyNestedSubs()
57+
58+
expect(notifications).toEqual([])
59+
})
60+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { shallowEqual } from '../src'
3+
4+
describe('Utils', () => {
5+
describe('shallowEqual', () => {
6+
it('should return true if arguments fields are equal', () => {
7+
expect(
8+
shallowEqual(
9+
{ a: 1, b: 2, c: undefined },
10+
{ a: 1, b: 2, c: undefined },
11+
),
12+
).toBe(true)
13+
14+
expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(
15+
true,
16+
)
17+
18+
const o = {}
19+
expect(shallowEqual({ a: 1, b: 2, c: o }, { a: 1, b: 2, c: o })).toBe(
20+
true,
21+
)
22+
23+
const d = function () {
24+
return 1
25+
}
26+
expect(
27+
shallowEqual({ a: 1, b: 2, c: o, d }, { a: 1, b: 2, c: o, d }),
28+
).toBe(true)
29+
})
30+
31+
it('should return false if arguments fields are different function identities', () => {
32+
expect(
33+
shallowEqual(
34+
{
35+
a: 1,
36+
b: 2,
37+
d: function () {
38+
return 1
39+
},
40+
},
41+
{
42+
a: 1,
43+
b: 2,
44+
d: function () {
45+
return 1
46+
},
47+
},
48+
),
49+
).toBe(false)
50+
})
51+
52+
it('should return false if first argument has too many keys', () => {
53+
expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false)
54+
})
55+
56+
it('should return false if second argument has too many keys', () => {
57+
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false)
58+
})
59+
60+
it('should return false if arguments have different keys', () => {
61+
expect(
62+
shallowEqual(
63+
{ a: 1, b: 2, c: undefined },
64+
{ a: 1, bb: 2, c: undefined },
65+
),
66+
).toBe(false)
67+
})
68+
69+
it('should compare two NaN values', () => {
70+
expect(shallowEqual(NaN, NaN)).toBe(true)
71+
})
72+
73+
it('should compare empty objects, with false', () => {
74+
expect(shallowEqual({}, false)).toBe(false)
75+
expect(shallowEqual(false, {})).toBe(false)
76+
expect(shallowEqual([], false)).toBe(false)
77+
expect(shallowEqual(false, [])).toBe(false)
78+
})
79+
80+
it('should compare two zero values', () => {
81+
expect(shallowEqual(0, 0)).toBe(true)
82+
})
83+
})
84+
})

packages/vue-redux/tests/use-selector.spec.tsx

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2-
import { defineComponent, h, inject, watchSyncEffect } from 'vue'
2+
import { Fragment, defineComponent, h, inject, watchSyncEffect } from 'vue'
33
import { createStore } from 'redux'
44
import { cleanup, render, waitFor } from '@testing-library/vue'
5-
import { ContextKey, provideStore as provideMock, useSelector } from '../src'
5+
import {
6+
ContextKey,
7+
provideStore as provideMock,
8+
shallowEqual,
9+
useSelector,
10+
} from '../src'
611
import type { Ref } from 'vue'
712
import type { Subscription } from '../src/utils/Subscription'
813
import type { TypedUseSelectorComposition } from '../src'
@@ -222,6 +227,86 @@ describe('Vue', () => {
222227
expect(renderedItems).toEqual([0, 1])
223228
})
224229
})
230+
231+
describe('performance optimizations and bail-outs', () => {
232+
it('defaults to ref-equality to prevent unnecessary updates', async () => {
233+
const state = {}
234+
const store = createStore(() => state)
235+
236+
const Comp = defineComponent(() => {
237+
const value = useSelector((s) => s)
238+
watchSyncEffect(() => {
239+
renderedItems.push(value.value)
240+
})
241+
return () => <div />
242+
})
243+
244+
const App = defineComponent(() => {
245+
provideMock({ store })
246+
return () => <Comp />
247+
})
248+
249+
render(<App />)
250+
251+
expect(renderedItems.length).toBe(1)
252+
253+
store.dispatch({ type: '' })
254+
255+
await waitFor(() => expect(renderedItems.length).toBe(1))
256+
})
257+
258+
it('allows other equality functions to prevent unnecessary updates', async () => {
259+
interface StateType {
260+
count: number
261+
stable: {}
262+
}
263+
const store = createStore(
264+
({ count, stable }: StateType = { count: -1, stable: {} }) => ({
265+
count: count + 1,
266+
stable,
267+
}),
268+
)
269+
270+
const Comp = defineComponent(() => {
271+
const value = useSelector(
272+
(s: StateType) => Object.keys(s),
273+
shallowEqual,
274+
)
275+
watchSyncEffect(() => {
276+
renderedItems.push(value.value)
277+
})
278+
return () => <div />
279+
})
280+
281+
const Comp2 = defineComponent(() => {
282+
const value = useSelector((s: StateType) => Object.keys(s), {
283+
equalityFn: shallowEqual,
284+
})
285+
watchSyncEffect(() => {
286+
renderedItems.push(value.value)
287+
})
288+
return () => <div />
289+
})
290+
291+
const App = defineComponent(() => {
292+
provideMock({ store })
293+
return () => (
294+
<>
295+
<Comp />
296+
<Comp2 />
297+
</>
298+
)
299+
})
300+
301+
render(<App />)
302+
303+
expect(renderedItems.length).toBe(2)
304+
305+
store.dispatch({ type: '' })
306+
307+
await waitFor(() => expect(renderedItems.length).toBe(2))
308+
})
309+
})
225310
})
226311
})
227312
})

0 commit comments

Comments
 (0)