mirror of
https://github.com/pmndrs/zustand.git
synced 2025-12-08 19:45:52 +00:00
187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import { useState } from 'react'
|
|
import { act, fireEvent, render, screen } from '@testing-library/react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { create } from 'zustand'
|
|
import { shallow, useShallow } from 'zustand/shallow'
|
|
import { createWithEqualityFn } from 'zustand/traditional'
|
|
|
|
describe('types', () => {
|
|
it('works with useBoundStore and array selector (#1107)', () => {
|
|
const useBoundStore = createWithEqualityFn(() => ({
|
|
villages: [] as { name: string }[],
|
|
}))
|
|
const Component = () => {
|
|
const villages = useBoundStore((state) => state.villages, shallow)
|
|
return <>{villages.length}</>
|
|
}
|
|
expect(Component).toBeDefined()
|
|
})
|
|
|
|
it('works with useBoundStore and string selector (#1107)', () => {
|
|
const useBoundStore = createWithEqualityFn(() => ({
|
|
refetchTimestamp: '',
|
|
}))
|
|
const Component = () => {
|
|
const refetchTimestamp = useBoundStore(
|
|
(state) => state.refetchTimestamp,
|
|
shallow,
|
|
)
|
|
return <>{refetchTimestamp.toUpperCase()}</>
|
|
}
|
|
expect(Component).toBeDefined()
|
|
})
|
|
})
|
|
|
|
describe('useShallow', () => {
|
|
const testUseShallowSimpleCallback = vi.fn()
|
|
const TestUseShallowSimple = ({
|
|
selector,
|
|
state,
|
|
}: {
|
|
state: Record<string, unknown>
|
|
selector: (state: Record<string, unknown>) => string[]
|
|
}) => {
|
|
const selectorOutput = selector(state)
|
|
const useShallowOutput = useShallow(selector)(state)
|
|
|
|
return (
|
|
<div
|
|
data-testid="test-shallow"
|
|
onClick={() =>
|
|
testUseShallowSimpleCallback({ selectorOutput, useShallowOutput })
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
beforeEach(() => {
|
|
testUseShallowSimpleCallback.mockClear()
|
|
})
|
|
|
|
it('input and output selectors always return shallow equal values', () => {
|
|
const { rerender } = render(
|
|
<TestUseShallowSimple state={{ a: 1, b: 2 }} selector={Object.keys} />,
|
|
)
|
|
|
|
expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(0)
|
|
fireEvent.click(screen.getByTestId('test-shallow'))
|
|
|
|
const firstRender = testUseShallowSimpleCallback.mock.lastCall?.[0]
|
|
|
|
expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1)
|
|
expect(firstRender).toBeTruthy()
|
|
expect(firstRender?.selectorOutput).toEqual(firstRender?.useShallowOutput)
|
|
|
|
rerender(
|
|
<TestUseShallowSimple
|
|
state={{ a: 1, b: 2, c: 3 }}
|
|
selector={Object.keys}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('test-shallow'))
|
|
expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2)
|
|
|
|
const secondRender = testUseShallowSimpleCallback.mock.lastCall?.[0]
|
|
|
|
expect(secondRender).toBeTruthy()
|
|
expect(secondRender?.selectorOutput).toEqual(secondRender?.useShallowOutput)
|
|
})
|
|
|
|
it('returns the previously computed instance when possible', () => {
|
|
const state = { a: 1, b: 2 }
|
|
const { rerender } = render(
|
|
<TestUseShallowSimple state={state} selector={Object.keys} />,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('test-shallow'))
|
|
expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(1)
|
|
const output1 =
|
|
testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput
|
|
expect(output1).toBeTruthy()
|
|
|
|
// Change selector, same output
|
|
rerender(
|
|
<TestUseShallowSimple
|
|
state={state}
|
|
selector={(state) => Object.keys(state)}
|
|
/>,
|
|
)
|
|
|
|
fireEvent.click(screen.getByTestId('test-shallow'))
|
|
expect(testUseShallowSimpleCallback).toHaveBeenCalledTimes(2)
|
|
|
|
const output2 =
|
|
testUseShallowSimpleCallback.mock.lastCall?.[0]?.useShallowOutput
|
|
expect(output2).toBeTruthy()
|
|
|
|
expect(output2).toBe(output1)
|
|
})
|
|
|
|
it('only re-renders if selector output has changed according to shallow', () => {
|
|
let countRenders = 0
|
|
const useMyStore = create(
|
|
(): Record<string, unknown> => ({ a: 1, b: 2, c: 3 }),
|
|
)
|
|
const TestShallow = ({
|
|
selector = (state) => Object.keys(state).sort(),
|
|
}: {
|
|
selector?: (state: Record<string, unknown>) => string[]
|
|
}) => {
|
|
const output = useMyStore(useShallow(selector))
|
|
|
|
++countRenders
|
|
|
|
return <div data-testid="test-shallow">{output.join(',')}</div>
|
|
}
|
|
|
|
expect(countRenders).toBe(0)
|
|
render(<TestShallow />)
|
|
|
|
expect(countRenders).toBe(1)
|
|
expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c')
|
|
|
|
act(() => {
|
|
useMyStore.setState({ a: 4 }) // This will not cause a re-render.
|
|
})
|
|
|
|
expect(countRenders).toBe(1)
|
|
|
|
act(() => {
|
|
useMyStore.setState({ d: 10 }) // This will cause a re-render.
|
|
})
|
|
|
|
expect(countRenders).toBe(2)
|
|
expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,d')
|
|
})
|
|
|
|
it('does not cause stale closure issues', () => {
|
|
const useMyStore = create(
|
|
(): Record<string, unknown> => ({ a: 1, b: 2, c: 3 }),
|
|
)
|
|
const TestShallowWithState = () => {
|
|
const [count, setCount] = useState(0)
|
|
const output = useMyStore(
|
|
useShallow((state) => Object.keys(state).concat([count.toString()])),
|
|
)
|
|
|
|
return (
|
|
<div
|
|
data-testid="test-shallow"
|
|
onClick={() => setCount((prev) => ++prev)}
|
|
>
|
|
{output.join(',')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
render(<TestShallowWithState />)
|
|
|
|
expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,0')
|
|
|
|
fireEvent.click(screen.getByTestId('test-shallow'))
|
|
|
|
expect(screen.getByTestId('test-shallow')).toHaveTextContent('a,b,c,1')
|
|
})
|
|
})
|