mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
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 <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>
This commit is contained in:
parent
2be79c9154
commit
e414f7ccf4
@ -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',
|
||||
|
||||
20
package.json
20
package.json
@ -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": {
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
13
src/react/shallow.ts
Normal file
13
src/react/shallow.ts
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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'`
|
||||
@ -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
src/vanilla/shallow.ts
Normal file
49
src/vanilla/shallow.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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)', () => {
|
||||
@ -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[] }]>()
|
||||
|
||||
136
tests/vanilla/basic.test.ts
Normal file
136
tests/vanilla/basic.test.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import type { StoreApi } from 'zustand/vanilla'
|
||||
|
||||
// To avoid include react deps on vanilla version
|
||||
vi.mock('react', () => ({}))
|
||||
|
||||
const consoleError = console.error
|
||||
afterEach(() => {
|
||||
console.error = consoleError
|
||||
})
|
||||
|
||||
it('create a store', () => {
|
||||
let params
|
||||
const result = createStore((...args) => {
|
||||
params = args
|
||||
return { value: null }
|
||||
})
|
||||
expect({ params, result }).toMatchInlineSnapshot(`
|
||||
{
|
||||
"params": [
|
||||
[Function],
|
||||
[Function],
|
||||
{
|
||||
"destroy": [Function],
|
||||
"getState": [Function],
|
||||
"setState": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
],
|
||||
"result": {
|
||||
"destroy": [Function],
|
||||
"getState": [Function],
|
||||
"setState": [Function],
|
||||
"subscribe": [Function],
|
||||
},
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
type CounterState = {
|
||||
count: number
|
||||
inc: () => void
|
||||
}
|
||||
|
||||
it('uses the store', async () => {
|
||||
const store = createStore<CounterState>((set) => ({
|
||||
count: 0,
|
||||
inc: () => set((state) => ({ count: state.count + 1 })),
|
||||
}))
|
||||
store.getState().inc()
|
||||
|
||||
expect(store.getState().count).toBe(1)
|
||||
})
|
||||
|
||||
it('can get the store', async () => {
|
||||
type State = {
|
||||
value: number
|
||||
getState1: () => State
|
||||
getState2: () => State
|
||||
}
|
||||
|
||||
const store = createStore<State>((_, get) => ({
|
||||
value: 1,
|
||||
getState1: () => get(),
|
||||
getState2: (): State => store.getState(),
|
||||
}))
|
||||
|
||||
expect(store.getState().getState1().value).toBe(1)
|
||||
expect(store.getState().getState2().value).toBe(1)
|
||||
})
|
||||
|
||||
it('can set the store', async () => {
|
||||
type State = {
|
||||
value: number
|
||||
setState1: StoreApi<State>['setState']
|
||||
setState2: StoreApi<State>['setState']
|
||||
}
|
||||
|
||||
const store = createStore<State>((set) => ({
|
||||
value: 1,
|
||||
setState1: (v) => set(v),
|
||||
setState2: (v): void => store.setState(v),
|
||||
}))
|
||||
|
||||
store.getState().setState1({ value: 2 })
|
||||
expect(store.getState().value).toBe(2)
|
||||
store.getState().setState2({ value: 3 })
|
||||
expect(store.getState().value).toBe(3)
|
||||
})
|
||||
|
||||
it('both NaN should not update', () => {
|
||||
const store = createStore<number>(() => NaN)
|
||||
const fn = vi.fn()
|
||||
|
||||
store.subscribe(fn)
|
||||
store.setState(NaN)
|
||||
|
||||
expect(fn).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('can set the store without merging', () => {
|
||||
const { setState, getState } = createStore<{ a: number } | { b: number }>(
|
||||
(_set) => ({
|
||||
a: 1,
|
||||
})
|
||||
)
|
||||
|
||||
// Should override the state instead of merging.
|
||||
setState({ b: 2 }, true)
|
||||
|
||||
expect(getState()).toEqual({ b: 2 })
|
||||
})
|
||||
|
||||
it('works with non-object state', () => {
|
||||
const store = createStore<number>(() => 1)
|
||||
const inc = () => store.setState((c) => c + 1)
|
||||
|
||||
inc()
|
||||
|
||||
expect(store.getState()).toBe(2)
|
||||
})
|
||||
|
||||
it('can destroy the store', () => {
|
||||
const { destroy, getState, setState, subscribe } = createStore(() => ({
|
||||
value: 1,
|
||||
}))
|
||||
|
||||
subscribe(() => {
|
||||
throw new Error('did not clear listener on destroy')
|
||||
})
|
||||
destroy()
|
||||
|
||||
setState({ value: 2 })
|
||||
expect(getState().value).toEqual(2)
|
||||
})
|
||||
105
tests/vanilla/shallow.test.tsx
Normal file
105
tests/vanilla/shallow.test.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shallow } from 'zustand/vanilla/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)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user