Skip to content

Commit e414f7c

Browse files
dbritto-devdai-shi
andauthored
fix(shallow): Extract shallow vanilla and react (#2097)
* 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 <[email protected]> * Minor changes * Update readmes * Update readmes * Update tests * Minor changes --------- Co-authored-by: Daishi Kato <[email protected]>
1 parent 2be79c9 commit e414f7c

File tree

9 files changed

+332
-166
lines changed

9 files changed

+332
-166
lines changed

docs/guides/prevent-rerenders-with-use-shallow.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ We can fix that using `useShallow`!
4545

4646
```js
4747
import { create } from 'zustand'
48-
import { useShallow } from 'zustand/shallow'
48+
import { useShallow } from 'zustand/react/shallow'
4949

5050
const useMeals = create(() => ({
5151
papaBear: 'large porridge-pot',

package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@
6565
"module": "./esm/shallow.js",
6666
"default": "./shallow.js"
6767
},
68+
"./vanilla/shallow": {
69+
"types": "./vanilla/shallow.d.ts",
70+
"import": {
71+
"types": "./esm/vanilla/shallow.d.mts",
72+
"default": "./esm/vanilla/shallow.mjs"
73+
},
74+
"module": "./esm/vanilla/shallow.js",
75+
"default": "./vanilla/shallow.js"
76+
},
77+
"./react/shallow": {
78+
"types": "./react/shallow.d.ts",
79+
"import": {
80+
"types": "./esm/react/shallow.d.mts",
81+
"default": "./esm/react/shallow.mjs"
82+
},
83+
"module": "./esm/react/shallow.js",
84+
"default": "./react/shallow.js"
85+
},
6886
"./traditional": {
6987
"types": "./traditional.d.ts",
7088
"import": {
@@ -93,6 +111,8 @@
93111
"build:middleware": "rollup -c --config-middleware",
94112
"build:middleware:immer": "rollup -c --config-middleware_immer",
95113
"build:shallow": "rollup -c --config-shallow",
114+
"build:vanilla:shallow": "rollup -c --config-vanilla_shallow",
115+
"build:react:shallow": "rollup -c --config-react_shallow",
96116
"build:traditional": "rollup -c --config-traditional",
97117
"build:context": "rollup -c --config-context",
98118
"postbuild": "yarn patch-d-ts && yarn copy && yarn patch-esm-ts",

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ If you want to construct a single object with multiple state-picks inside, simil
8888

8989
```jsx
9090
import { create } from 'zustand'
91-
import { useShallow } from 'zustand/shallow'
91+
import { useShallow } from 'zustand/react/shallow'
9292

9393
const useBearStore = create((set) => ({
9494
bears: 0,

src/react/shallow.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useRef } from 'react'
2+
import { shallow } from '../vanilla/shallow.ts'
3+
4+
export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
5+
const prev = useRef<U>()
6+
7+
return (state) => {
8+
const next = selector(state)
9+
return shallow(prev.current, next)
10+
? (prev.current as U)
11+
: (prev.current = next)
12+
}
13+
}

src/shallow.ts

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,8 @@
1-
import { useRef } from 'react'
1+
import { shallow } from './vanilla/shallow.ts'
22

3-
export function shallow<T>(objA: T, objB: T) {
4-
if (Object.is(objA, objB)) {
5-
return true
6-
}
7-
if (
8-
typeof objA !== 'object' ||
9-
objA === null ||
10-
typeof objB !== 'object' ||
11-
objB === null
12-
) {
13-
return false
14-
}
15-
16-
if (objA instanceof Map && objB instanceof Map) {
17-
if (objA.size !== objB.size) return false
18-
19-
for (const [key, value] of objA) {
20-
if (!Object.is(value, objB.get(key))) {
21-
return false
22-
}
23-
}
24-
return true
25-
}
26-
27-
if (objA instanceof Set && objB instanceof Set) {
28-
if (objA.size !== objB.size) return false
29-
30-
for (const value of objA) {
31-
if (!objB.has(value)) {
32-
return false
33-
}
34-
}
35-
return true
36-
}
37-
38-
const keysA = Object.keys(objA)
39-
if (keysA.length !== Object.keys(objB).length) {
40-
return false
41-
}
42-
for (let i = 0; i < keysA.length; i++) {
43-
if (
44-
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
45-
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
46-
) {
47-
return false
48-
}
49-
}
50-
return true
51-
}
3+
// We will export this in v5 and remove default export
4+
// export { shallow } from './vanilla/shallow.ts'
5+
// export { useShallow } from './react/shallow.ts'
526

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

65-
export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
66-
const prev = useRef<U>()
67-
68-
return (state) => {
69-
const next = selector(state)
70-
return shallow(prev.current, next)
71-
? (prev.current as U)
72-
: (prev.current = next)
73-
}
74-
}
19+
export { shallow }

src/vanilla/shallow.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export function shallow<T>(objA: T, objB: T) {
2+
if (Object.is(objA, objB)) {
3+
return true
4+
}
5+
if (
6+
typeof objA !== 'object' ||
7+
objA === null ||
8+
typeof objB !== 'object' ||
9+
objB === null
10+
) {
11+
return false
12+
}
13+
14+
if (objA instanceof Map && objB instanceof Map) {
15+
if (objA.size !== objB.size) return false
16+
17+
for (const [key, value] of objA) {
18+
if (!Object.is(value, objB.get(key))) {
19+
return false
20+
}
21+
}
22+
return true
23+
}
24+
25+
if (objA instanceof Set && objB instanceof Set) {
26+
if (objA.size !== objB.size) return false
27+
28+
for (const value of objA) {
29+
if (!objB.has(value)) {
30+
return false
31+
}
32+
}
33+
return true
34+
}
35+
36+
const keysA = Object.keys(objA)
37+
if (keysA.length !== Object.keys(objB).length) {
38+
return false
39+
}
40+
for (let i = 0; i < keysA.length; i++) {
41+
if (
42+
!Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
43+
!Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
44+
) {
45+
return false
46+
}
47+
}
48+
return true
49+
}

tests/shallow.test.tsx

Lines changed: 2 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,99 +2,8 @@ import { useState } from 'react'
22
import { act, fireEvent, render } from '@testing-library/react'
33
import { beforeEach, describe, expect, it, vi } from 'vitest'
44
import { create } from 'zustand'
5-
import { shallow, useShallow } from 'zustand/shallow'
6-
7-
describe('shallow', () => {
8-
it('compares primitive values', () => {
9-
expect(shallow(true, true)).toBe(true)
10-
expect(shallow(true, false)).toBe(false)
11-
12-
expect(shallow(1, 1)).toBe(true)
13-
expect(shallow(1, 2)).toBe(false)
14-
15-
expect(shallow('zustand', 'zustand')).toBe(true)
16-
expect(shallow('zustand', 'redux')).toBe(false)
17-
})
18-
19-
it('compares objects', () => {
20-
expect(shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123 })).toBe(
21-
true
22-
)
23-
24-
expect(
25-
shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', foobar: true })
26-
).toBe(false)
27-
28-
expect(
29-
shallow({ foo: 'bar', asd: 123 }, { foo: 'bar', asd: 123, foobar: true })
30-
).toBe(false)
31-
})
32-
33-
it('compares arrays', () => {
34-
expect(shallow([1, 2, 3], [1, 2, 3])).toBe(true)
35-
36-
expect(shallow([1, 2, 3], [2, 3, 4])).toBe(false)
37-
38-
expect(
39-
shallow([{ foo: 'bar' }, { asd: 123 }], [{ foo: 'bar' }, { asd: 123 }])
40-
).toBe(false)
41-
42-
expect(shallow([{ foo: 'bar' }], [{ foo: 'bar', asd: 123 }])).toBe(false)
43-
})
44-
45-
it('compares Maps', () => {
46-
function createMap<T extends object>(obj: T) {
47-
return new Map(Object.entries(obj))
48-
}
49-
50-
expect(
51-
shallow(
52-
createMap({ foo: 'bar', asd: 123 }),
53-
createMap({ foo: 'bar', asd: 123 })
54-
)
55-
).toBe(true)
56-
57-
expect(
58-
shallow(
59-
createMap({ foo: 'bar', asd: 123 }),
60-
createMap({ foo: 'bar', foobar: true })
61-
)
62-
).toBe(false)
63-
64-
expect(
65-
shallow(
66-
createMap({ foo: 'bar', asd: 123 }),
67-
createMap({ foo: 'bar', asd: 123, foobar: true })
68-
)
69-
).toBe(false)
70-
})
71-
72-
it('compares Sets', () => {
73-
expect(shallow(new Set(['bar', 123]), new Set(['bar', 123]))).toBe(true)
74-
75-
expect(shallow(new Set(['bar', 123]), new Set(['bar', 2]))).toBe(false)
76-
77-
expect(shallow(new Set(['bar', 123]), new Set(['bar', 123, true]))).toBe(
78-
false
79-
)
80-
})
81-
82-
it('compares functions', () => {
83-
function firstFnCompare() {
84-
return { foo: 'bar' }
85-
}
86-
87-
function secondFnCompare() {
88-
return { foo: 'bar' }
89-
}
90-
91-
expect(shallow(firstFnCompare, firstFnCompare)).toBe(true)
92-
93-
expect(shallow(secondFnCompare, secondFnCompare)).toBe(true)
94-
95-
expect(shallow(firstFnCompare, secondFnCompare)).toBe(false)
96-
})
97-
})
5+
import { useShallow } from 'zustand/react/shallow'
6+
import { shallow } from 'zustand/vanilla/shallow'
987

998
describe('types', () => {
1009
it('works with useBoundStore and array selector (#1107)', () => {
@@ -123,17 +32,6 @@ describe('types', () => {
12332
})
12433
})
12534

126-
describe('unsupported cases', () => {
127-
it('date', () => {
128-
expect(
129-
shallow(
130-
new Date('2022-07-19T00:00:00.000Z'),
131-
new Date('2022-07-20T00:00:00.000Z')
132-
)
133-
).not.toBe(false)
134-
})
135-
})
136-
13735
describe('useShallow', () => {
13836
const testUseShallowSimpleCallback =
13937
vi.fn<[{ selectorOutput: string[]; useShallowOutput: string[] }]>()

0 commit comments

Comments
 (0)