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..111ea1f 100644 --- a/packages/zustand-x/src/createStore.ts +++ b/packages/zustand-x/src/createStore.ts @@ -1,10 +1,10 @@ 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 { immerMiddleware } from './middlewares/immer.middleware'; @@ -70,9 +70,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 +92,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..63d292a --- /dev/null +++ b/packages/zustand-x/src/useStore.spec.tsx @@ -0,0 +1,135 @@ +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 ( + + ); + }; + + 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); + }); + }); + + 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(); + }); + }); +}); 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); +});