[v5]: follow React "standard" way with breaking behavioral change (#2395)

* [v5]: follow React "standard" way with breaking behavioral change

* add test
This commit is contained in:
Daishi Kato 2024-03-09 09:16:35 +09:00 committed by GitHub
parent b19acdfab7
commit f87eec594d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 26 deletions

View File

@ -12,29 +12,13 @@ import type {
StoreMutatorIdentifier,
} from './vanilla.ts'
const { useDebugValue, useMemo, useSyncExternalStore } = ReactExports
const { useDebugValue, useSyncExternalStore } = ReactExports
type ExtractState<S> = S extends { getState: () => infer T } ? T : never
type ReadonlyStoreApi<T> = Pick<StoreApi<T>, 'getState' | 'subscribe'>
const identity = <T>(arg: T): T => arg
const useMemoSelector = <TState, StateSlice>(
getState: () => TState,
selector: (state: TState) => StateSlice,
) =>
useMemo(() => {
let prev: readonly [TState, StateSlice] | undefined
return () => {
const state = getState()
if (!prev || !Object.is(prev[0], state)) {
prev = [state, selector(state)]
}
return prev[1]
}
}, [getState, selector])
export function useStore<S extends StoreApi<unknown>>(api: S): ExtractState<S>
export function useStore<S extends StoreApi<unknown>, U>(
@ -48,8 +32,8 @@ export function useStore<TState, StateSlice>(
) {
const slice = useSyncExternalStore(
api.subscribe,
useMemoSelector(api.getState, selector),
useMemoSelector(api.getInitialState, selector),
() => selector(api.getState()),
() => selector(api.getInitialState()),
)
useDebugValue(slice)
return slice

View File

@ -472,9 +472,47 @@ it('can set the store without merging', () => {
expect(getState()).toEqual({ b: 2 })
})
it('only calls selectors when necessary', async () => {
it('only calls selectors when necessary with static selector', async () => {
type State = { a: number; b: number }
const useBoundStore = create<State>(() => ({ a: 0, b: 0 }))
const useBoundStore = createWithEqualityFn<State>(() => ({ a: 0, b: 0 }))
const { setState } = useBoundStore
let staticSelectorCallCount = 0
function staticSelector(s: State) {
staticSelectorCallCount++
return s.a
}
function Component() {
useBoundStore(staticSelector)
return (
<>
<div>static: {staticSelectorCallCount}</div>
</>
)
}
const { rerender, findByText } = render(
<>
<Component />
</>,
)
await findByText('static: 1')
rerender(
<>
<Component />
</>,
)
await findByText('static: 1')
act(() => setState({ a: 1, b: 1 }))
await findByText('static: 2')
})
it('only calls selectors when necessary (traditional)', async () => {
type State = { a: number; b: number }
const useBoundStore = createWithEqualityFn<State>(() => ({ a: 0, b: 0 }))
const { setState } = useBoundStore
let inlineSelectorCallCount = 0
let staticSelectorCallCount = 0

View File

@ -19,11 +19,9 @@ const useBearStore = create<BearStoreState & BearStoreAction>((set) => ({
}))
function Counter() {
const { bears, increasePopulation } = useBearStore(
({ bears, increasePopulation }) => ({
bears,
increasePopulation,
}),
const bears = useBearStore(({ bears }) => bears)
const increasePopulation = useBearStore(
({ increasePopulation }) => increasePopulation,
)
useEffect(() => {