From 1540bfd29344998a20b47c38e7b4524b7124d964 Mon Sep 17 00:00:00 2001 From: Christian van der Loo Date: Sun, 25 Aug 2024 08:52:37 -0400 Subject: [PATCH] fix: assert types on `set` and `get` --- tests/middlewareTypes.test.tsx | 1403 ++++++++++++++++---------------- 1 file changed, 705 insertions(+), 698 deletions(-) diff --git a/tests/middlewareTypes.test.tsx b/tests/middlewareTypes.test.tsx index e87c5f62ab..d54c27dcd3 100644 --- a/tests/middlewareTypes.test.tsx +++ b/tests/middlewareTypes.test.tsx @@ -1,713 +1,720 @@ /* eslint @typescript-eslint/no-unused-expressions: off */ // FIXME /* eslint react-compiler/react-compiler: off */ -import { describe, expect, expectTypeOf, it } from 'vitest' -import { create } from 'zustand' -import type { StateCreator, StoreApi, StoreMutatorIdentifier } from 'zustand' +import { describe, expect, expectTypeOf, it } from "vitest"; +import { create } from "zustand"; +import type { StateCreator, StoreApi, StoreMutatorIdentifier } from "zustand"; import { - combine, - devtools, - persist, - redux, - subscribeWithSelector, -} from 'zustand/middleware' -import { immer } from 'zustand/middleware/immer' -import { createStore } from 'zustand/vanilla' + combine, + devtools, + persist, + redux, + subscribeWithSelector, +} from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; +import { createStore } from "zustand/vanilla"; type CounterState = { - count: number - inc: () => void -} + count: number; + inc: () => void; +}; type ExampleStateCreator = < - Mps extends [StoreMutatorIdentifier, unknown][] = [], - Mcs extends [StoreMutatorIdentifier, unknown][] = [], - U = T, + Mps extends [StoreMutatorIdentifier, unknown][] = [], + Mcs extends [StoreMutatorIdentifier, unknown][] = [], + U = T, >( - f: StateCreator, -) => StateCreator + f: StateCreator, +) => StateCreator; -type Write = Omit & U +type Write = Omit & U; type StoreModifyAllButSetState = S extends { - getState: () => infer T + getState: () => infer T; } - ? Omit, 'setState'> - : never + ? Omit, "setState"> + : never; -declare module 'zustand/vanilla' { - interface StoreMutators { - 'org/example': Write> - } +declare module "zustand/vanilla" { + interface StoreMutators { + "org/example": Write>; + } } -describe('counter state spec (no middleware)', () => { - it('no middleware', () => { - const useBoundStore = create((set, get) => ({ - count: 0, - inc: () => set({ count: get().count + 1 }, false), - })) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - return <> - } - TestComponent - }) -}) - -describe('counter state spec (single middleware)', () => { - it('immer', () => { - const useBoundStore = create()( - immer((set, get) => ({ - count: 0, - inc: () => - set((state) => { - state.count = get().count + 1 - }), - })), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - immer(() => ({ count: 0 })), - ) - expect(testSubtyping).toBeDefined() - - const exampleMiddleware = ((initializer) => - initializer) as ExampleStateCreator - - const testDerivedSetStateType = create()( - exampleMiddleware( - immer((set, get) => ({ - count: 0, - inc: () => - set((state) => { - state.count = get().count + 1 - }), - })), - ), - ) - expect(testDerivedSetStateType).toBeDefined() - // the type of the `getState` should include our new property - expectTypeOf(testDerivedSetStateType.getState()).toMatchTypeOf<{ - additional: number - }>() - // the type of the `setState` should not include our new property - expectTypeOf< - Parameters[0] - >().not.toMatchTypeOf<{ - additional: number - }>() - }) - - it('redux', () => { - const useBoundStore = create( - redux<{ count: number }, { type: 'INC' }>( - (state, action) => { - switch (action.type) { - case 'INC': - return { ...state, count: state.count + 1 } - default: - return state - } - }, - { count: 0 }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.dispatch)({ type: 'INC' }) - useBoundStore().dispatch({ type: 'INC' }) - useBoundStore.dispatch({ type: 'INC' }) - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - redux((x) => x, { count: 0 }), - ) - expect(testSubtyping).toBeDefined() - }) - - it('devtools', () => { - const useBoundStore = create()( - devtools( - (set, get) => ({ - count: 0, - inc: () => set({ count: get().count + 1 }, false, 'inc'), - }), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - devtools(() => ({ count: 0 })), - ) - expect(testSubtyping).toBeDefined() - }) - - it('subscribeWithSelector', () => { - const useBoundStore = create()( - subscribeWithSelector((set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false), - })), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - subscribeWithSelector(() => ({ count: 0 })), - ) - expect(testSubtyping).toBeDefined() - }) - - it('combine', () => { - const useBoundStore = create( - combine({ count: 1 }, (set, get) => ({ - inc: () => set({ count: get().count + 1 }, false), - })), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - combine({ count: 0 }, () => ({})), - ) - expect(testSubtyping).toBeDefined() - }) - - it('persist', () => { - const useBoundStore = create()( - persist( - (set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false), - }), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - - const testSubtyping: StoreApi = createStore( - persist(() => ({ count: 0 }), { name: 'prefix' }), - ) - expect(testSubtyping).toBeDefined() - }) - - it('persist with partialize', () => { - const useBoundStore = create()( - persist( - (set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false), - }), - { name: 'prefix', partialize: (s) => s.count }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.persist.hasHydrated() - useBoundStore.persist.setOptions({ - // @ts-expect-error to test if the partialized state is inferred as number - partialize: () => 'not-a-number', - }) - return <> - } - TestComponent - }) - - it('persist without custom api (#638)', () => { - const useBoundStore = create()( - persist( - (set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false), - }), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - return <> - } - TestComponent - }) -}) - -describe('counter state spec (double middleware)', () => { - it('immer & devtools', () => { - const useBoundStore = create()( - immer( - devtools( - (set, get) => ({ - count: 0, - inc: () => - set( - (state) => { - state.count = get().count + 1 - }, - false, - { type: 'inc', by: 1 }, - ), - }), - { name: 'prefix' }, - ), - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - }) - - it('devtools & redux', () => { - const useBoundStore = create( - devtools( - redux( - (state, action: { type: 'INC' }) => { - switch (action.type) { - case 'INC': - return { ...state, count: state.count + 1 } - default: - return state - } - }, - { count: 0 }, - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.dispatch)({ type: 'INC' }) - useBoundStore().dispatch({ type: 'INC' }) - useBoundStore.dispatch({ type: 'INC' }) - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - }) - - it('devtools & combine', () => { - const useBoundStore = create( - devtools( - combine({ count: 1 }, (set, get) => ({ - inc: () => set({ count: get().count + 1 }, false, 'inc'), - })), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - }) - - it('subscribeWithSelector & combine', () => { - const useBoundStore = create( - subscribeWithSelector( - combine({ count: 1 }, (set, get) => ({ - inc: () => set({ count: get().count + 1 }, false), - })), - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - return <> - } - TestComponent - }) - - it('devtools & subscribeWithSelector', () => { - const useBoundStore = create()( - devtools( - subscribeWithSelector((set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false, 'inc'), - })), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - }) - - it('devtools & persist', () => { - const useBoundStore = create()( - devtools( - persist( - (set, get) => ({ - count: 1, - inc: () => set({ count: get().count + 1 }, false, 'inc'), - }), - { name: 'count' }, - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.setState({ count: 0 }, false, 'reset') - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) -}) - -describe('counter state spec (triple middleware)', () => { - it('devtools & persist & immer', () => { - const useBoundStore = create()( - devtools( - persist( - immer((set, get) => ({ - count: 0, - inc: () => - set((state) => { - state.count = get().count + 1 - }), - })), - { name: 'count' }, - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.setState({ count: 0 }, false, 'reset') - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) - - it('devtools & subscribeWithSelector & combine', () => { - const useBoundStore = create( - devtools( - subscribeWithSelector( - combine({ count: 1 }, (set, get) => ({ - inc: () => set({ count: get().count + 1 }, false, 'inc'), - })), - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - useBoundStore.setState({ count: 0 }, false, 'reset') - return <> - } - TestComponent - }) - - it('devtools & subscribeWithSelector & persist', () => { - const useBoundStore = create()( - devtools( - subscribeWithSelector( - persist( - (set, get) => ({ - count: 0, - inc: () => set({ count: get().count + 1 }, false), - }), - { name: 'count' }, - ), - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - useBoundStore.setState({ count: 0 }, false, 'reset') - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) -}) - -describe('counter state spec (quadruple middleware)', () => { - it('devtools & subscribeWithSelector & persist & immer (#616)', () => { - const useBoundStore = create()( - devtools( - subscribeWithSelector( - persist( - immer((set, get) => ({ - count: 0, - inc: () => - set((state) => { - state.count = get().count + 1 - }), - })), - { name: 'count' }, - ), - ), - { name: 'prefix' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - useBoundStore.setState({ count: 0 }, false, 'reset') - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) -}) - -describe('more complex state spec with subscribeWithSelector', () => { - it('#619, #632', () => { - const useBoundStore = create( - subscribeWithSelector( - persist( - () => ({ - foo: true, - }), - { name: 'name' }, - ), - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.foo) - useBoundStore().foo - useBoundStore.getState().foo - useBoundStore.subscribe( - (state) => state.foo, - (foo) => console.log(foo), - ) - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) - - it('#631', () => { - type MyState = { - foo: number | null - } - const useBoundStore = create()( - subscribeWithSelector( - () => - ({ - foo: 1, - }) as MyState, // NOTE: Asserting the entire state works too. - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.foo) - useBoundStore().foo - useBoundStore.getState().foo - useBoundStore.subscribe( - (state) => state.foo, - (foo) => console.log(foo), - ) - return <> - } - TestComponent - }) - - it('#650', () => { - type MyState = { - token: string | undefined - authenticated: boolean - authenticate: (username: string, password: string) => Promise - } - const useBoundStore = create()( - persist( - (set) => ({ - token: undefined, - authenticated: false, - authenticate: async (_username, _password) => { - set({ authenticated: true }) - }, - }), - { name: 'auth-store' }, - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.authenticated) - useBoundStore((s) => s.authenticate)('u', 'p') - useBoundStore().authenticated - useBoundStore().authenticate('u', 'p') - useBoundStore.getState().authenticated - useBoundStore.getState().authenticate('u', 'p') - return <> - } - TestComponent - }) -}) - -describe('create with explicitly annotated mutators', () => { - it('subscribeWithSelector & persist', () => { - const useBoundStore = create< - CounterState, - [ - ['zustand/subscribeWithSelector', never], - ['zustand/persist', CounterState], - ] - >( - subscribeWithSelector( - persist( - (set, get) => ({ - count: 0, - inc: () => set({ count: get().count + 1 }, false), - }), - { name: 'count' }, - ), - ), - ) - const TestComponent = () => { - useBoundStore((s) => s.count) * 2 - useBoundStore((s) => s.inc)() - useBoundStore().count * 2 - useBoundStore().inc() - useBoundStore.getState().count * 2 - useBoundStore.getState().inc() - useBoundStore.subscribe( - (state) => state.count, - (count) => console.log(count * 2), - ) - useBoundStore.setState({ count: 0 }, false) - useBoundStore.persist.hasHydrated() - return <> - } - TestComponent - }) -}) +describe("counter state spec (no middleware)", () => { + it("no middleware", () => { + const useBoundStore = create((set, get) => ({ + count: 0, + inc: () => set({ count: get().count + 1 }, false), + })); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + return <>; + }; + TestComponent; + }); +}); + +describe("counter state spec (single middleware)", () => { + it("immer", () => { + const useBoundStore = create()( + immer((set, get) => ({ + count: 0, + inc: () => + set((state) => { + state.count = get().count + 1; + }), + })), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + immer(() => ({ count: 0 })), + ); + expect(testSubtyping).toBeDefined(); + + const exampleMiddleware = ((initializer) => + initializer) as ExampleStateCreator; + + const testDerivedSetStateType = create()( + exampleMiddleware( + immer((set, get) => ({ + count: 0, + inc: () => + set((state) => { + state.count = get().count + 1; + type OmitFn = Exclude any>; + expectTypeOf< + OmitFn[0]> + >().not.toMatchTypeOf<{ additional: number }>(); + expectTypeOf>().toMatchTypeOf<{ + additional: number; + }>(); + }), + })), + ), + ); + expect(testDerivedSetStateType).toBeDefined(); + // the type of the `getState` should include our new property + expectTypeOf(testDerivedSetStateType.getState()).toMatchTypeOf<{ + additional: number; + }>(); + // the type of the `setState` should not include our new property + expectTypeOf< + Parameters[0] + >().not.toMatchTypeOf<{ + additional: number; + }>(); + }); + + it("redux", () => { + const useBoundStore = create( + redux<{ count: number }, { type: "INC" }>( + (state, action) => { + switch (action.type) { + case "INC": + return { ...state, count: state.count + 1 }; + default: + return state; + } + }, + { count: 0 }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.dispatch)({ type: "INC" }); + useBoundStore().dispatch({ type: "INC" }); + useBoundStore.dispatch({ type: "INC" }); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + redux((x) => x, { count: 0 }), + ); + expect(testSubtyping).toBeDefined(); + }); + + it("devtools", () => { + const useBoundStore = create()( + devtools( + (set, get) => ({ + count: 0, + inc: () => set({ count: get().count + 1 }, false, "inc"), + }), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + devtools(() => ({ count: 0 })), + ); + expect(testSubtyping).toBeDefined(); + }); + + it("subscribeWithSelector", () => { + const useBoundStore = create()( + subscribeWithSelector((set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false), + })), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + subscribeWithSelector(() => ({ count: 0 })), + ); + expect(testSubtyping).toBeDefined(); + }); + + it("combine", () => { + const useBoundStore = create( + combine({ count: 1 }, (set, get) => ({ + inc: () => set({ count: get().count + 1 }, false), + })), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + combine({ count: 0 }, () => ({})), + ); + expect(testSubtyping).toBeDefined(); + }); + + it("persist", () => { + const useBoundStore = create()( + persist( + (set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + + const testSubtyping: StoreApi = createStore( + persist(() => ({ count: 0 }), { name: "prefix" }), + ); + expect(testSubtyping).toBeDefined(); + }); + + it("persist with partialize", () => { + const useBoundStore = create()( + persist( + (set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: "prefix", partialize: (s) => s.count }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.persist.hasHydrated(); + useBoundStore.persist.setOptions({ + // @ts-expect-error to test if the partialized state is inferred as number + partialize: () => "not-a-number", + }); + return <>; + }; + TestComponent; + }); + + it("persist without custom api (#638)", () => { + const useBoundStore = create()( + persist( + (set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + return <>; + }; + TestComponent; + }); +}); + +describe("counter state spec (double middleware)", () => { + it("immer & devtools", () => { + const useBoundStore = create()( + immer( + devtools( + (set, get) => ({ + count: 0, + inc: () => + set( + (state) => { + state.count = get().count + 1; + }, + false, + { type: "inc", by: 1 }, + ), + }), + { name: "prefix" }, + ), + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + }); + + it("devtools & redux", () => { + const useBoundStore = create( + devtools( + redux( + (state, action: { type: "INC" }) => { + switch (action.type) { + case "INC": + return { ...state, count: state.count + 1 }; + default: + return state; + } + }, + { count: 0 }, + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.dispatch)({ type: "INC" }); + useBoundStore().dispatch({ type: "INC" }); + useBoundStore.dispatch({ type: "INC" }); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + }); + + it("devtools & combine", () => { + const useBoundStore = create( + devtools( + combine({ count: 1 }, (set, get) => ({ + inc: () => set({ count: get().count + 1 }, false, "inc"), + })), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + }); + + it("subscribeWithSelector & combine", () => { + const useBoundStore = create( + subscribeWithSelector( + combine({ count: 1 }, (set, get) => ({ + inc: () => set({ count: get().count + 1 }, false), + })), + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + return <>; + }; + TestComponent; + }); + + it("devtools & subscribeWithSelector", () => { + const useBoundStore = create()( + devtools( + subscribeWithSelector((set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false, "inc"), + })), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + }); + + it("devtools & persist", () => { + const useBoundStore = create()( + devtools( + persist( + (set, get) => ({ + count: 1, + inc: () => set({ count: get().count + 1 }, false, "inc"), + }), + { name: "count" }, + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.setState({ count: 0 }, false, "reset"); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); +}); + +describe("counter state spec (triple middleware)", () => { + it("devtools & persist & immer", () => { + const useBoundStore = create()( + devtools( + persist( + immer((set, get) => ({ + count: 0, + inc: () => + set((state) => { + state.count = get().count + 1; + }), + })), + { name: "count" }, + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.setState({ count: 0 }, false, "reset"); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); + + it("devtools & subscribeWithSelector & combine", () => { + const useBoundStore = create( + devtools( + subscribeWithSelector( + combine({ count: 1 }, (set, get) => ({ + inc: () => set({ count: get().count + 1 }, false, "inc"), + })), + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + useBoundStore.setState({ count: 0 }, false, "reset"); + return <>; + }; + TestComponent; + }); + + it("devtools & subscribeWithSelector & persist", () => { + const useBoundStore = create()( + devtools( + subscribeWithSelector( + persist( + (set, get) => ({ + count: 0, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: "count" }, + ), + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + useBoundStore.setState({ count: 0 }, false, "reset"); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); +}); + +describe("counter state spec (quadruple middleware)", () => { + it("devtools & subscribeWithSelector & persist & immer (#616)", () => { + const useBoundStore = create()( + devtools( + subscribeWithSelector( + persist( + immer((set, get) => ({ + count: 0, + inc: () => + set((state) => { + state.count = get().count + 1; + }), + })), + { name: "count" }, + ), + ), + { name: "prefix" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + useBoundStore.setState({ count: 0 }, false, "reset"); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); +}); + +describe("more complex state spec with subscribeWithSelector", () => { + it("#619, #632", () => { + const useBoundStore = create( + subscribeWithSelector( + persist( + () => ({ + foo: true, + }), + { name: "name" }, + ), + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.foo); + useBoundStore().foo; + useBoundStore.getState().foo; + useBoundStore.subscribe( + (state) => state.foo, + (foo) => console.log(foo), + ); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); + + it("#631", () => { + type MyState = { + foo: number | null; + }; + const useBoundStore = create()( + subscribeWithSelector( + () => + ({ + foo: 1, + }) as MyState, // NOTE: Asserting the entire state works too. + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.foo); + useBoundStore().foo; + useBoundStore.getState().foo; + useBoundStore.subscribe( + (state) => state.foo, + (foo) => console.log(foo), + ); + return <>; + }; + TestComponent; + }); + + it("#650", () => { + type MyState = { + token: string | undefined; + authenticated: boolean; + authenticate: (username: string, password: string) => Promise; + }; + const useBoundStore = create()( + persist( + (set) => ({ + token: undefined, + authenticated: false, + authenticate: async (_username, _password) => { + set({ authenticated: true }); + }, + }), + { name: "auth-store" }, + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.authenticated); + useBoundStore((s) => s.authenticate)("u", "p"); + useBoundStore().authenticated; + useBoundStore().authenticate("u", "p"); + useBoundStore.getState().authenticated; + useBoundStore.getState().authenticate("u", "p"); + return <>; + }; + TestComponent; + }); +}); + +describe("create with explicitly annotated mutators", () => { + it("subscribeWithSelector & persist", () => { + const useBoundStore = create< + CounterState, + [ + ["zustand/subscribeWithSelector", never], + ["zustand/persist", CounterState], + ] + >( + subscribeWithSelector( + persist( + (set, get) => ({ + count: 0, + inc: () => set({ count: get().count + 1 }, false), + }), + { name: "count" }, + ), + ), + ); + const TestComponent = () => { + useBoundStore((s) => s.count) * 2; + useBoundStore((s) => s.inc)(); + useBoundStore().count * 2; + useBoundStore().inc(); + useBoundStore.getState().count * 2; + useBoundStore.getState().inc(); + useBoundStore.subscribe( + (state) => state.count, + (count) => console.log(count * 2), + ); + useBoundStore.setState({ count: 0 }, false); + useBoundStore.persist.hasHydrated(); + return <>; + }; + TestComponent; + }); +});