Skip to content

Commit

Permalink
feat: add shallowEqual export
Browse files Browse the repository at this point in the history
  • Loading branch information
crutchcorn committed Sep 14, 2024
1 parent 2328188 commit b820017
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 15 deletions.
2 changes: 2 additions & 0 deletions packages/vue-redux/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './compositions/use-dispatch'
export * from './compositions/use-selector'
export * from './compositions/use-redux-context'
export * from './types'
export * from './utils/shallowEqual'
export type { Subscription } from './utils/Subscription'
14 changes: 5 additions & 9 deletions packages/vue-redux/src/utils/Subscription.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { defaultNoopBatch as batch } from './batch'

// encapsulates the subscription logic for connecting a component to the redux store, as
// well as nesting subscriptions of descendant components, so that we can ensure the
// ancestor components re-render before descendants
Expand All @@ -23,13 +21,11 @@ function createListenerCollection() {
},

notify() {
batch(() => {
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
})
let listener = first
while (listener) {
listener.callback()
listener = listener.next
}
},

get() {
Expand Down
4 changes: 0 additions & 4 deletions packages/vue-redux/src/utils/batch.ts

This file was deleted.

36 changes: 36 additions & 0 deletions packages/vue-redux/src/utils/shallowEqual.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
function is(x: unknown, y: unknown) {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y
} else {
return x !== x && y !== y
}
}

export function shallowEqual(objA: any, objB: any) {
if (is(objA, objB)) return true

if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}

const keysA = Object.keys(objA)
const keysB = Object.keys(objB)

if (keysA.length !== keysB.length) return false

for (let i = 0; i < keysA.length; i++) {
if (
!(Object.prototype.hasOwnProperty as Function).call(objB, keysA[i]) ||
!is(objA[keysA[i]!], objB[keysA[i]!])
) {
return false
}
}

return true
}
60 changes: 60 additions & 0 deletions packages/vue-redux/tests/Subscription.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createSubscription } from '../src/utils/Subscription'
import type { Subscription } from '../src/utils/Subscription'
import type { Store } from 'redux'

describe('Subscription', () => {
let notifications: string[]
let store: Store
let parent: Subscription

beforeEach(() => {
notifications = []
store = { subscribe: () => vi.fn() } as unknown as Store

parent = createSubscription(store)
parent.onStateChange = () => {}
parent.trySubscribe()
})

function subscribeChild(name: string) {
const child = createSubscription(store, parent)
child.onStateChange = () => notifications.push(name)
child.trySubscribe()
return child
}

it('listeners are notified in order', () => {
subscribeChild('child1')
subscribeChild('child2')
subscribeChild('child3')
subscribeChild('child4')

parent.notifyNestedSubs()

expect(notifications).toEqual(['child1', 'child2', 'child3', 'child4'])
})

it('listeners can be unsubscribed', () => {
const child1 = subscribeChild('child1')
const child2 = subscribeChild('child2')
const child3 = subscribeChild('child3')

child2.tryUnsubscribe()
parent.notifyNestedSubs()

expect(notifications).toEqual(['child1', 'child3'])
notifications.length = 0

child1.tryUnsubscribe()
parent.notifyNestedSubs()

expect(notifications).toEqual(['child3'])
notifications.length = 0

child3.tryUnsubscribe()
parent.notifyNestedSubs()

expect(notifications).toEqual([])
})
})
84 changes: 84 additions & 0 deletions packages/vue-redux/tests/shallowEqual.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest'
import { shallowEqual } from '../src'

describe('Utils', () => {
describe('shallowEqual', () => {
it('should return true if arguments fields are equal', () => {
expect(
shallowEqual(
{ a: 1, b: 2, c: undefined },
{ a: 1, b: 2, c: undefined },
),
).toBe(true)

expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(
true,
)

const o = {}
expect(shallowEqual({ a: 1, b: 2, c: o }, { a: 1, b: 2, c: o })).toBe(
true,
)

const d = function () {
return 1
}
expect(
shallowEqual({ a: 1, b: 2, c: o, d }, { a: 1, b: 2, c: o, d }),
).toBe(true)
})

it('should return false if arguments fields are different function identities', () => {
expect(
shallowEqual(
{
a: 1,
b: 2,
d: function () {
return 1
},
},
{
a: 1,
b: 2,
d: function () {
return 1
},
},
),
).toBe(false)
})

it('should return false if first argument has too many keys', () => {
expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false)
})

it('should return false if second argument has too many keys', () => {
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false)
})

it('should return false if arguments have different keys', () => {
expect(
shallowEqual(
{ a: 1, b: 2, c: undefined },
{ a: 1, bb: 2, c: undefined },
),
).toBe(false)
})

it('should compare two NaN values', () => {
expect(shallowEqual(NaN, NaN)).toBe(true)
})

it('should compare empty objects, with false', () => {
expect(shallowEqual({}, false)).toBe(false)
expect(shallowEqual(false, {})).toBe(false)
expect(shallowEqual([], false)).toBe(false)
expect(shallowEqual(false, [])).toBe(false)
})

it('should compare two zero values', () => {
expect(shallowEqual(0, 0)).toBe(true)
})
})
})
89 changes: 87 additions & 2 deletions packages/vue-redux/tests/use-selector.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defineComponent, h, inject, watchSyncEffect } from 'vue'
import { Fragment, defineComponent, h, inject, watchSyncEffect } from 'vue'
import { createStore } from 'redux'
import { cleanup, render, waitFor } from '@testing-library/vue'
import { ContextKey, provideStore as provideMock, useSelector } from '../src'
import {
ContextKey,
provideStore as provideMock,
shallowEqual,
useSelector,
} from '../src'
import type { Ref } from 'vue'
import type { Subscription } from '../src/utils/Subscription'
import type { TypedUseSelectorComposition } from '../src'
Expand Down Expand Up @@ -222,6 +227,86 @@ describe('Vue', () => {
expect(renderedItems).toEqual([0, 1])
})
})

describe('performance optimizations and bail-outs', () => {
it('defaults to ref-equality to prevent unnecessary updates', async () => {
const state = {}
const store = createStore(() => state)

const Comp = defineComponent(() => {
const value = useSelector((s) => s)
watchSyncEffect(() => {
renderedItems.push(value.value)
})
return () => <div />
})

const App = defineComponent(() => {
provideMock({ store })
return () => <Comp />
})

render(<App />)

expect(renderedItems.length).toBe(1)

store.dispatch({ type: '' })

await waitFor(() => expect(renderedItems.length).toBe(1))
})

it('allows other equality functions to prevent unnecessary updates', async () => {
interface StateType {
count: number
stable: {}
}
const store = createStore(
({ count, stable }: StateType = { count: -1, stable: {} }) => ({
count: count + 1,
stable,
}),
)

const Comp = defineComponent(() => {
const value = useSelector(
(s: StateType) => Object.keys(s),
shallowEqual,
)
watchSyncEffect(() => {
renderedItems.push(value.value)
})
return () => <div />
})

const Comp2 = defineComponent(() => {
const value = useSelector((s: StateType) => Object.keys(s), {
equalityFn: shallowEqual,
})
watchSyncEffect(() => {
renderedItems.push(value.value)
})
return () => <div />
})

const App = defineComponent(() => {
provideMock({ store })
return () => (
<>
<Comp />
<Comp2 />
</>
)
})

render(<App />)

expect(renderedItems.length).toBe(2)

store.dispatch({ type: '' })

await waitFor(() => expect(renderedItems.length).toBe(2))
})
})
})
})
})

0 comments on commit b820017

Please sign in to comment.