From 92c710989bd9aba9f605c5c0cb15f4878285515b Mon Sep 17 00:00:00 2001 From: zbeyens Date: Sun, 10 Dec 2023 17:23:31 +0100 Subject: [PATCH 1/2] fix --- .changeset/curly-numbers-help.md | 6 + config/eslint/bases/react.cjs | 4 +- packages/zustand-x/src/createStore.ts | 21 ++-- packages/zustand-x/src/useStore.spec.tsx | 103 ++++++++++++++++++ .../src/utils/generateStateActions.ts | 4 +- .../src/utils/generateStateGetSelectors.ts | 4 +- .../src/utils/generateStateHookSelectors.ts | 13 ++- .../generateStateTrackedHooksSelectors.ts | 8 +- scripts/setupTests.ts | 4 +- 9 files changed, 146 insertions(+), 21 deletions(-) create mode 100644 .changeset/curly-numbers-help.md create mode 100644 packages/zustand-x/src/useStore.spec.tsx diff --git a/.changeset/curly-numbers-help.md b/.changeset/curly-numbers-help.md new file mode 100644 index 0000000..5675b1e --- /dev/null +++ b/.changeset/curly-numbers-help.md @@ -0,0 +1,6 @@ +--- +'zustand-x': patch +--- + +- Fixes #60 – `[DEPRECATED] Passing a vanilla store will be unsupported in a future version` +- Support `equalityFn` towards v5. See https://github.com/pmndrs/zustand/discussions/1937. diff --git a/config/eslint/bases/react.cjs b/config/eslint/bases/react.cjs index 1512fb2..a864ac5 100644 --- a/config/eslint/bases/react.cjs +++ b/config/eslint/bases/react.cjs @@ -50,8 +50,8 @@ module.exports = { 'mdx/no-unescaped-entities': 'off', 'mdx/no-unused-expressions': 'off', - 'react-hooks/exhaustive-deps': 'warn', - 'react-hooks/rules-of-hooks': 'error', + // 'react-hooks/exhaustive-deps': 'warn', + // 'react-hooks/rules-of-hooks': 'error', 'react/button-has-type': [ 'error', { diff --git a/packages/zustand-x/src/createStore.ts b/packages/zustand-x/src/createStore.ts index c24b3b1..d26e32d 100644 --- a/packages/zustand-x/src/createStore.ts +++ b/packages/zustand-x/src/createStore.ts @@ -1,12 +1,14 @@ import { enableMapSet, setAutoFreeze } from 'immer'; import { createTrackedSelector } from 'react-tracked'; -import { create } from 'zustand'; import { devtools as devtoolsMiddleware, persist as persistMiddleware, } from 'zustand/middleware'; +import { useStoreWithEqualityFn } from 'zustand/traditional'; import { createStore as createVanillaStore } from 'zustand/vanilla'; +import 'zustand/vanilla'; + import { immerMiddleware } from './middlewares/immer.middleware'; import { ImmerStoreApi, @@ -70,9 +72,14 @@ export const createStore = pipe(createState as any, ...middlewares) as ImmerStoreApi; const store = pipeMiddlewares(() => initialState); - const useStore = create(store as any) as UseImmerStore; + const useStore = ((selector, equalityFn) => + useStoreWithEqualityFn( + store as any, + selector as any, + equalityFn as any + )) as UseImmerStore; - const stateActions = generateStateActions(useStore, name); + const stateActions = generateStateActions(store, name); const mergeState: MergeState = (state, actionName) => { store.setState( @@ -87,13 +94,13 @@ export const createStore = store.setState(fn, actionName || `@@${name}/setState`); }; - const hookSelectors = generateStateHookSelectors(useStore); - const getterSelectors = generateStateGetSelectors(useStore); + const hookSelectors = generateStateHookSelectors(useStore, store); + const getterSelectors = generateStateGetSelectors(store); const useTrackedStore = createTrackedSelector(useStore); const trackedHooksSelectors = generateStateTrackedHooksSelectors( - useStore, - useTrackedStore + useTrackedStore, + store ); const api = { diff --git a/packages/zustand-x/src/useStore.spec.tsx b/packages/zustand-x/src/useStore.spec.tsx new file mode 100644 index 0000000..0379836 --- /dev/null +++ b/packages/zustand-x/src/useStore.spec.tsx @@ -0,0 +1,103 @@ +import '@testing-library/jest-dom'; + +import React from 'react'; +import { act, render, renderHook } from '@testing-library/react'; + +import { createZustandStore } from './createStore'; + +describe('createAtomStore', () => { + describe('single provider', () => { + type MyTestStoreValue = { + name: string; + age: number; + }; + + const INITIAL_NAME = 'John'; + const INITIAL_AGE = 42; + + const initialTestStoreValue: MyTestStoreValue = { + name: INITIAL_NAME, + age: INITIAL_AGE, + }; + + const store = createZustandStore('myTestStore')(initialTestStoreValue); + const useSelectors = () => store.use; + const actions = store.set; + const selectors = store.get; + + const ReadOnlyConsumer = () => { + const name = useSelectors().name(); + const age = useSelectors().age(); + + return ( +
+ {name} + {age} +
+ ); + }; + + const WriteOnlyConsumer = () => { + return ( + + ); + }; + + const MUTABLE_PROVIDER_INITIAL_AGE = 19; + const MUTABLE_PROVIDER_NEW_AGE = 20; + + // const MutableProvider = ({ children }: { children: ReactNode }) => { + // const [age, setAge] = useState(MUTABLE_PROVIDER_INITIAL_AGE); + // + // return ( + // <> + // {children} + // + // + // + // ); + // }; + + beforeEach(() => { + renderHook(() => actions.name(INITIAL_NAME)); + renderHook(() => actions.age(INITIAL_AGE)); + }); + + it('read only', () => { + const { getByText } = render(); + + expect(getByText(INITIAL_NAME)).toBeInTheDocument(); + expect(getByText(INITIAL_AGE)).toBeInTheDocument(); + }); + + it('actions', () => { + const { getByText } = render( + <> + + + + ); + expect(getByText(INITIAL_NAME)).toBeInTheDocument(); + expect(getByText(INITIAL_AGE)).toBeInTheDocument(); + + act(() => getByText('consumerSetAge').click()); + + expect(getByText(INITIAL_NAME)).toBeInTheDocument(); + expect(getByText(INITIAL_AGE + 1)).toBeInTheDocument(); + expect(store.store.getState().age).toBe(INITIAL_AGE + 1); + }); + }); +}); diff --git a/packages/zustand-x/src/utils/generateStateActions.ts b/packages/zustand-x/src/utils/generateStateActions.ts index 289401e..28da8e9 100644 --- a/packages/zustand-x/src/utils/generateStateActions.ts +++ b/packages/zustand-x/src/utils/generateStateActions.ts @@ -1,7 +1,7 @@ -import { SetRecord, State, UseImmerStore } from '../types'; +import { ImmerStoreApi, SetRecord, State } from '../types'; export const generateStateActions = ( - store: UseImmerStore, + store: ImmerStoreApi, storeName: string ) => { const actions: SetRecord = {} as any; diff --git a/packages/zustand-x/src/utils/generateStateGetSelectors.ts b/packages/zustand-x/src/utils/generateStateGetSelectors.ts index 3962510..aa3c2f4 100644 --- a/packages/zustand-x/src/utils/generateStateGetSelectors.ts +++ b/packages/zustand-x/src/utils/generateStateGetSelectors.ts @@ -1,7 +1,7 @@ -import { GetRecord, State, UseImmerStore } from '../types'; +import { GetRecord, ImmerStoreApi, State } from '../types'; export const generateStateGetSelectors = ( - store: UseImmerStore + store: ImmerStoreApi ) => { const selectors: GetRecord = {} as any; diff --git a/packages/zustand-x/src/utils/generateStateHookSelectors.ts b/packages/zustand-x/src/utils/generateStateHookSelectors.ts index 9b8b971..582fef6 100644 --- a/packages/zustand-x/src/utils/generateStateHookSelectors.ts +++ b/packages/zustand-x/src/utils/generateStateHookSelectors.ts @@ -1,14 +1,21 @@ -import { EqualityChecker, GetRecord, State, UseImmerStore } from '../types'; +import { + EqualityChecker, + GetRecord, + ImmerStoreApi, + State, + UseImmerStore, +} from '../types'; export const generateStateHookSelectors = ( - store: UseImmerStore + useStore: UseImmerStore, + store: ImmerStoreApi ) => { const selectors: GetRecord = {} as any; Object.keys((store as any).getState()).forEach((key) => { // selectors[`use${capitalize(key)}`] = () => selectors[key as keyof T] = (equalityFn?: EqualityChecker) => { - return store((state: T) => state[key as keyof T], equalityFn); + return useStore((state: T) => state[key as keyof T], equalityFn); }; }); diff --git a/packages/zustand-x/src/utils/generateStateTrackedHooksSelectors.ts b/packages/zustand-x/src/utils/generateStateTrackedHooksSelectors.ts index 93b8157..34d3710 100644 --- a/packages/zustand-x/src/utils/generateStateTrackedHooksSelectors.ts +++ b/packages/zustand-x/src/utils/generateStateTrackedHooksSelectors.ts @@ -1,14 +1,14 @@ -import { GetRecord, State, UseImmerStore } from '../types'; +import { GetRecord, ImmerStoreApi, State } from '../types'; export const generateStateTrackedHooksSelectors = ( - store: UseImmerStore, - trackedStore: () => T + useTrackedStore: () => T, + store: ImmerStoreApi ) => { const selectors: GetRecord = {} as any; Object.keys((store as any).getState()).forEach((key) => { selectors[key as keyof T] = () => { - return trackedStore()[key as keyof T]; + return useTrackedStore()[key as keyof T]; }; }); diff --git a/scripts/setupTests.ts b/scripts/setupTests.ts index 8825b65..edfc075 100644 --- a/scripts/setupTests.ts +++ b/scripts/setupTests.ts @@ -1,3 +1,5 @@ import '@testing-library/jest-dom'; -jest.spyOn(global.console, 'warn').mockImplementation(() => jest.fn()); +jest.spyOn(global.console, 'warn').mockImplementation((message) => { + throw new Error(message); +}); From e4422000e7d12e55b0c81ccf41041c256c00a1aa Mon Sep 17 00:00:00 2001 From: zbeyens Date: Sun, 10 Dec 2023 17:28:41 +0100 Subject: [PATCH 2/2] fix --- packages/zustand-x/src/createStore.ts | 2 - packages/zustand-x/src/useStore.spec.tsx | 72 +++++++++++++++++------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/zustand-x/src/createStore.ts b/packages/zustand-x/src/createStore.ts index d26e32d..111ea1f 100644 --- a/packages/zustand-x/src/createStore.ts +++ b/packages/zustand-x/src/createStore.ts @@ -7,8 +7,6 @@ import { import { useStoreWithEqualityFn } from 'zustand/traditional'; import { createStore as createVanillaStore } from 'zustand/vanilla'; -import 'zustand/vanilla'; - import { immerMiddleware } from './middlewares/immer.middleware'; import { ImmerStoreApi, diff --git a/packages/zustand-x/src/useStore.spec.tsx b/packages/zustand-x/src/useStore.spec.tsx index 0379836..63d292a 100644 --- a/packages/zustand-x/src/useStore.spec.tsx +++ b/packages/zustand-x/src/useStore.spec.tsx @@ -51,26 +51,6 @@ describe('createAtomStore', () => { ); }; - const MUTABLE_PROVIDER_INITIAL_AGE = 19; - const MUTABLE_PROVIDER_NEW_AGE = 20; - - // const MutableProvider = ({ children }: { children: ReactNode }) => { - // const [age, setAge] = useState(MUTABLE_PROVIDER_INITIAL_AGE); - // - // return ( - // <> - // {children} - // - // - // - // ); - // }; - beforeEach(() => { renderHook(() => actions.name(INITIAL_NAME)); renderHook(() => actions.age(INITIAL_AGE)); @@ -100,4 +80,56 @@ describe('createAtomStore', () => { expect(store.store.getState().age).toBe(INITIAL_AGE + 1); }); }); + + describe('multiple unrelated stores', () => { + type MyFirstTestStoreValue = { name: string }; + type MySecondTestStoreValue = { age: number }; + + const initialFirstTestStoreValue: MyFirstTestStoreValue = { + name: 'My name', + }; + + const initialSecondTestStoreValue: MySecondTestStoreValue = { + age: 72, + }; + + const myFirstTestStoreStore = createZustandStore('myFirstTestStore')( + initialFirstTestStoreValue + ); + const mySecondTestStoreStore = createZustandStore('mySecondTestStore')( + initialSecondTestStoreValue + ); + + const FirstReadOnlyConsumer = () => { + const name = myFirstTestStoreStore.use.name(); + + return ( +
+ {name} +
+ ); + }; + + const SecondReadOnlyConsumer = () => { + const age = mySecondTestStoreStore.use.age(); + + return ( +
+ {age} +
+ ); + }; + + it('returns the value for the correct store', () => { + const { getByText } = render( + <> + + + + ); + + expect(getByText('My name')).toBeInTheDocument(); + expect(getByText(72)).toBeInTheDocument(); + }); + }); });