Skip to content

Commit

Permalink
Merge pull request #69 from udecode/fix/60
Browse files Browse the repository at this point in the history
Fix #60
  • Loading branch information
zbeyens authored Dec 10, 2023
2 parents d80fd72 + e442200 commit 9b1cb99
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 21 deletions.
6 changes: 6 additions & 0 deletions .changeset/curly-numbers-help.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions config/eslint/bases/react.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
19 changes: 12 additions & 7 deletions packages/zustand-x/src/createStore.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -70,9 +70,14 @@ export const createStore =
pipe(createState as any, ...middlewares) as ImmerStoreApi<T>;

const store = pipeMiddlewares(() => initialState);
const useStore = create(store as any) as UseImmerStore<T>;
const useStore = ((selector, equalityFn) =>
useStoreWithEqualityFn(
store as any,
selector as any,
equalityFn as any
)) as UseImmerStore<T>;

const stateActions = generateStateActions(useStore, name);
const stateActions = generateStateActions(store, name);

const mergeState: MergeState<T> = (state, actionName) => {
store.setState(
Expand All @@ -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 = {
Expand Down
135 changes: 135 additions & 0 deletions packages/zustand-x/src/useStore.spec.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<span>{name}</span>
<span>{age}</span>
</div>
);
};

const WriteOnlyConsumer = () => {
return (
<button
type="button"
onClick={() => {
selectors.age();
actions.age(selectors.age() + 1);
}}
>
consumerSetAge
</button>
);
};

beforeEach(() => {
renderHook(() => actions.name(INITIAL_NAME));
renderHook(() => actions.age(INITIAL_AGE));
});

it('read only', () => {
const { getByText } = render(<ReadOnlyConsumer />);

expect(getByText(INITIAL_NAME)).toBeInTheDocument();
expect(getByText(INITIAL_AGE)).toBeInTheDocument();
});

it('actions', () => {
const { getByText } = render(
<>
<ReadOnlyConsumer />
<WriteOnlyConsumer />
</>
);
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 (
<div>
<span>{name}</span>
</div>
);
};

const SecondReadOnlyConsumer = () => {
const age = mySecondTestStoreStore.use.age();

return (
<div>
<span>{age}</span>
</div>
);
};

it('returns the value for the correct store', () => {
const { getByText } = render(
<>
<FirstReadOnlyConsumer />
<SecondReadOnlyConsumer />
</>
);

expect(getByText('My name')).toBeInTheDocument();
expect(getByText(72)).toBeInTheDocument();
});
});
});
4 changes: 2 additions & 2 deletions packages/zustand-x/src/utils/generateStateActions.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SetRecord, State, UseImmerStore } from '../types';
import { ImmerStoreApi, SetRecord, State } from '../types';

export const generateStateActions = <T extends State>(
store: UseImmerStore<T>,
store: ImmerStoreApi<T>,
storeName: string
) => {
const actions: SetRecord<T> = {} as any;
Expand Down
4 changes: 2 additions & 2 deletions packages/zustand-x/src/utils/generateStateGetSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { GetRecord, State, UseImmerStore } from '../types';
import { GetRecord, ImmerStoreApi, State } from '../types';

export const generateStateGetSelectors = <T extends State>(
store: UseImmerStore<T>
store: ImmerStoreApi<T>
) => {
const selectors: GetRecord<T> = {} as any;

Expand Down
13 changes: 10 additions & 3 deletions packages/zustand-x/src/utils/generateStateHookSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { EqualityChecker, GetRecord, State, UseImmerStore } from '../types';
import {
EqualityChecker,
GetRecord,
ImmerStoreApi,
State,
UseImmerStore,
} from '../types';

export const generateStateHookSelectors = <T extends State>(
store: UseImmerStore<T>
useStore: UseImmerStore<T>,
store: ImmerStoreApi<T>
) => {
const selectors: GetRecord<T> = {} as any;

Object.keys((store as any).getState()).forEach((key) => {
// selectors[`use${capitalize(key)}`] = () =>
selectors[key as keyof T] = (equalityFn?: EqualityChecker<T[keyof T]>) => {
return store((state: T) => state[key as keyof T], equalityFn);
return useStore((state: T) => state[key as keyof T], equalityFn);
};
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { GetRecord, State, UseImmerStore } from '../types';
import { GetRecord, ImmerStoreApi, State } from '../types';

export const generateStateTrackedHooksSelectors = <T extends State>(
store: UseImmerStore<T>,
trackedStore: () => T
useTrackedStore: () => T,
store: ImmerStoreApi<T>
) => {
const selectors: GetRecord<T> = {} 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];
};
});

Expand Down
4 changes: 3 additions & 1 deletion scripts/setupTests.ts
Original file line number Diff line number Diff line change
@@ -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);
});

0 comments on commit 9b1cb99

Please sign in to comment.