Skip to content

Commit

Permalink
fix(shallow): Extract shallow vanilla and react (#2097)
Browse files Browse the repository at this point in the history
* Update readmes

* Splitting shallow in two modules

* Update tests

* Minor changes

* Minor changes

* Rename shadow.tests.tsx to shallow.test.tsx

* Add new entrypoint for shallow/react

* Update structure

* Update shallow to export from vanilla and react

* Add vanilla/shallow and react/shallow entrypoints

* Update tests

* Update readmes

* Update src/shallow.ts

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>

* Minor changes

* Update readmes

* Update readmes

* Update tests

* Minor changes

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
  • Loading branch information
dbritto-dev and dai-shi authored Oct 5, 2023
1 parent 2be79c9 commit e414f7c
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 166 deletions.
2 changes: 1 addition & 1 deletion docs/guides/prevent-rerenders-with-use-shallow.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ We can fix that using `useShallow`!

```js
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
import { useShallow } from 'zustand/react/shallow'

const useMeals = create(() => ({
papaBear: 'large porridge-pot',
Expand Down
20 changes: 20 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@
"module": "./esm/shallow.js",
"default": "./shallow.js"
},
"./vanilla/shallow": {
"types": "./vanilla/shallow.d.ts",
"import": {
"types": "./esm/vanilla/shallow.d.mts",
"default": "./esm/vanilla/shallow.mjs"
},
"module": "./esm/vanilla/shallow.js",
"default": "./vanilla/shallow.js"
},
"./react/shallow": {
"types": "./react/shallow.d.ts",
"import": {
"types": "./esm/react/shallow.d.mts",
"default": "./esm/react/shallow.mjs"
},
"module": "./esm/react/shallow.js",
"default": "./react/shallow.js"
},
"./traditional": {
"types": "./traditional.d.ts",
"import": {
Expand Down Expand Up @@ -93,6 +111,8 @@
"build:middleware": "rollup -c --config-middleware",
"build:middleware:immer": "rollup -c --config-middleware_immer",
"build:shallow": "rollup -c --config-shallow",
"build:vanilla:shallow": "rollup -c --config-vanilla_shallow",
"build:react:shallow": "rollup -c --config-react_shallow",
"build:traditional": "rollup -c --config-traditional",
"build:context": "rollup -c --config-context",
"postbuild": "yarn patch-d-ts && yarn copy && yarn patch-esm-ts",
Expand Down
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ If you want to construct a single object with multiple state-picks inside, simil

```jsx
import { create } from 'zustand'
import { useShallow } from 'zustand/shallow'
import { useShallow } from 'zustand/react/shallow'

const useBearStore = create((set) => ({
bears: 0,
Expand Down
13 changes: 13 additions & 0 deletions src/react/shallow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useRef } from 'react'
import { shallow } from '../vanilla/shallow.ts'

export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
const prev = useRef<U>()

return (state) => {
const next = selector(state)
return shallow(prev.current, next)
? (prev.current as U)
: (prev.current = next)
}
}
65 changes: 5 additions & 60 deletions src/shallow.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,8 @@
import { useRef } from 'react'
import { shallow } from './vanilla/shallow.ts'

export function shallow<T>(objA: T, objB: T) {
if (Object.is(objA, objB)) {
return true
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}

if (objA instanceof Map && objB instanceof Map) {
if (objA.size !== objB.size) return false

for (const [key, value] of objA) {
if (!Object.is(value, objB.get(key))) {
return false
}
}
return true
}

if (objA instanceof Set && objB instanceof Set) {
if (objA.size !== objB.size) return false

for (const value of objA) {
if (!objB.has(value)) {
return false
}
}
return true
}

const keysA = Object.keys(objA)
if (keysA.length !== Object.keys(objB).length) {
return false
}
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
) {
return false
}
}
return true
}
// We will export this in v5 and remove default export
// export { shallow } from './vanilla/shallow.ts'
// export { useShallow } from './react/shallow.ts'

/**
* @deprecated Use `import { shallow } from 'zustand/shallow'`
Expand All @@ -62,13 +16,4 @@ export default ((objA, objB) => {
return shallow(objA, objB)
}) as typeof shallow

export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
const prev = useRef<U>()

return (state) => {
const next = selector(state)
return shallow(prev.current, next)
? (prev.current as U)
: (prev.current = next)
}
}
export { shallow }
49 changes: 49 additions & 0 deletions src/vanilla/shallow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export function shallow<T>(objA: T, objB: T) {
if (Object.is(objA, objB)) {
return true
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false
}

if (objA instanceof Map && objB instanceof Map) {
if (objA.size !== objB.size) return false

for (const [key, value] of objA) {
if (!Object.is(value, objB.get(key))) {
return false
}
}
return true
}

if (objA instanceof Set && objB instanceof Set) {
if (objA.size !== objB.size) return false

for (const value of objA) {
if (!objB.has(value)) {
return false
}
}
return true
}

const keysA = Object.keys(objA)
if (keysA.length !== Object.keys(objB).length) {
return false
}
for (let i = 0; i < keysA.length; i++) {
if (
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
) {
return false
}
}
return true
}
106 changes: 2 additions & 104 deletions tests/shallow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,99 +2,8 @@ import { useState } from 'react'
import { act, fireEvent, render } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { create } from 'zustand'
import { shallow, useShallow } from 'zustand/shallow'

describe('shallow', () => {
it('compares primitive values', () => {
expect(shallow(true, true)).toBe(true)
expect(shallow(true, false)).toBe(false)

expect(shallow(1, 1)).toBe(true)
expect(shallow(1, 2)).toBe(false)

expect(shallow('zustand', 'zustand')).toBe(true)
expect(shallow('zustand', 'redux')).toBe(false)
})

it('compares objects', () => {
expect(shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123 })).toBe(
true
)

expect(
shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', foobar: true })
).toBe(false)

expect(
shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123, foobar: true })
).toBe(false)
})

it('compares arrays', () => {
expect(shallow([1, 2, 3], [1, 2, 3])).toBe(true)

expect(shallow([1, 2, 3], [2, 3, 4])).toBe(false)

expect(
shallow([{ foo: 'bar' }, { asd: 123 }], [{ foo: 'bar' }, { asd: 123 }])
).toBe(false)

expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false)
})

it('compares Maps', () => {
function createMap<T extends object>(obj: T) {
return new Map(Object.entries(obj))
}

expect(
shallow(
createMap({ foo: 'bar', asd: 123 }),
createMap({ foo: 'bar', asd: 123 })
)
).toBe(true)

expect(
shallow(
createMap({ foo: 'bar', asd: 123 }),
createMap({ foo: 'bar', foobar: true })
)
).toBe(false)

expect(
shallow(
createMap({ foo: 'bar', asd: 123 }),
createMap({ foo: 'bar', asd: 123, foobar: true })
)
).toBe(false)
})

it('compares Sets', () => {
expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true)

expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false)

expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe(
false
)
})

it('compares functions', () => {
function firstFnCompare() {
return { foo: 'bar' }
}

function secondFnCompare() {
return { foo: 'bar' }
}

expect(shallow(firstFnCompare, firstFnCompare)).toBe(true)

expect(shallow(secondFnCompare, secondFnCompare)).toBe(true)

expect(shallow(firstFnCompare, secondFnCompare)).toBe(false)
})
})
import { useShallow } from 'zustand/react/shallow'
import { shallow } from 'zustand/vanilla/shallow'

describe('types', () => {
it('works with useBoundStore and array selector (#1107)', () => {
Expand Down Expand Up @@ -123,17 +32,6 @@ describe('types', () => {
})
})

describe('unsupported cases', () => {
it('date', () => {
expect(
shallow(
new Date('2022-07-19T00:00:00.000Z'),
new Date('2022-07-20T00:00:00.000Z')
)
).not.toBe(false)
})
})

describe('useShallow', () => {
const testUseShallowSimpleCallback =
vi.fn<[{ selectorOutput: string[]; useShallowOutput: string[] }]>()
Expand Down
Loading

1 comment on commit e414f7c

@vercel
Copy link

@vercel vercel bot commented on e414f7c Oct 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.