Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #60 #69

Merged
merged 2 commits into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});