diff --git a/.changeset/blue-moose-hug.md b/.changeset/blue-moose-hug.md new file mode 100644 index 0000000..8640883 --- /dev/null +++ b/.changeset/blue-moose-hug.md @@ -0,0 +1,5 @@ +--- +'jotai-x': patch +--- + +Fix: Return value of `useStore` is not memorized diff --git a/packages/jotai-x/src/createAtomStore.spec.tsx b/packages/jotai-x/src/createAtomStore.spec.tsx index 7a4a453..527493f 100644 --- a/packages/jotai-x/src/createAtomStore.spec.tsx +++ b/packages/jotai-x/src/createAtomStore.spec.tsx @@ -1102,6 +1102,14 @@ describe('createAtomStore', () => { expect(getByText('Change Callback new')).toBeInTheDocument(); expect(getByText('useBecameFriends: true')).toBeInTheDocument(); }); + + it('returns a stable store from useNameStore', () => { + const { result, rerender } = renderHook(useMyTestStoreStore); + const first = result.current; + rerender(); + const second = result.current; + expect(first === second).toBeTruthy(); + }); }); describe('scoped providers', () => { diff --git a/packages/jotai-x/src/createAtomStore.ts b/packages/jotai-x/src/createAtomStore.ts index 9471d45..3ccecf0 100644 --- a/packages/jotai-x/src/createAtomStore.ts +++ b/packages/jotai-x/src/createAtomStore.ts @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { getDefaultStore, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { selectAtom, useHydrateAtoms } from 'jotai/utils'; @@ -473,6 +473,15 @@ const convertScopeShorthand = ( ? { scope: optionsOrScope } : optionsOrScope; +const useConvertScopeShorthand: typeof convertScopeShorthand = ( + optionsOrScope +) => { + const convertedOptions = convertScopeShorthand(optionsOrScope); + // Works because all values are primitives + // eslint-disable-next-line react-compiler/react-compiler + return useMemo(() => convertedOptions, Object.values(convertedOptions)); +}; + const identity = (x: any) => x; export interface CreateAtomStoreOptions< @@ -755,118 +764,121 @@ Please wrap them with useCallback or configure the deps array correctly.` }; const useStoreApi: UseStoreApi = (options = {}) => { - const scopedOptions = convertScopeShorthand(options); - const store = useStore(scopedOptions); - - return { - // store.useValue() - ...(withStoreAndOptions( - atomsOfUseValue, - getUseValueIndex, - store, - scopedOptions - ) as UseKeyValueApis), - // store.get() - ...(withStoreAndOptions( - atomsOfGet, - getGetIndex, - store, - scopedOptions - ) as GetKeyApis), - // store.useSet() - ...(withStoreAndOptions( - atomsOfUseSet, - getUseSetIndex, - store, - scopedOptions - ) as UseSetKeyApis), - // store.set(...args) - ...(withStoreAndOptions( - atomsOfSet, - getSetIndex, - store, - scopedOptions - ) as SetKeyApis), - // store.useState() - ...(withStoreAndOptions( - atomsOfUseState, - getUseStateIndex, - store, - scopedOptions - ) as UseKeyStateApis), - // store.subscribe(callback) - ...(withStoreAndOptions( - atomsOfSubscribe, - getSubscribeIndex, - store, - scopedOptions - ) as SubscribeKeyApis), - // store.useValue('key') - useValue: withKeyAndStoreAndOptions( - atomsOfUseValue, - store, - scopedOptions - ) as UseParamKeyValueApi, - // store.get('key') - get: withKeyAndStoreAndOptions( - atomsOfGet, - store, - scopedOptions - ) as GetParamKeyApi, - // store.useSet('key') - useSet: withKeyAndStoreAndOptions( - atomsOfUseSet, - store, - scopedOptions - ) as UseSetParamKeyApi, - // store.set('key', ...args) - set: withKeyAndStoreAndOptions( - atomsOfSet, - store, - scopedOptions - ) as SetParamKeyApi, - // store.useState('key') - useState: withKeyAndStoreAndOptions( - atomsOfUseState, - store, - scopedOptions - ) as UseParamKeyStateApi, - // store.subscribe('key', callback) - subscribe: withKeyAndStoreAndOptions( - atomsOfSubscribe, - store, - scopedOptions - ) as SubscribeParamKeyApi, - // store.useAtomValue(atomConfig) - useAtomValue: ((atomConfig, selector, equalityFnOrDeps, deps) => - // eslint-disable-next-line react-compiler/react-compiler - useAtomValueWithStore( - atomConfig, + const convertedOptions = useConvertScopeShorthand(options); + const store = useStore(convertedOptions); + + return useMemo( + () => ({ + // store.useValue() + ...(withStoreAndOptions( + atomsOfUseValue, + getUseValueIndex, store, - scopedOptions, - selector, - equalityFnOrDeps, - deps - )) as UseAtomParamValueApi, - // store.getAtom(atomConfig) - getAtom: (atomConfig) => - getAtomWithStore(atomConfig, store, scopedOptions), - // store.useSetAtom(atomConfig) - useSetAtom: (atomConfig) => - // eslint-disable-next-line react-compiler/react-compiler - useSetAtomWithStore(atomConfig, store, scopedOptions), - // store.setAtom(atomConfig, ...args) - setAtom: (atomConfig) => - setAtomWithStore(atomConfig, store, scopedOptions), - // store.useAtomState(atomConfig) - useAtomState: (atomConfig) => - // eslint-disable-next-line react-compiler/react-compiler - useAtomStateWithStore(atomConfig, store, scopedOptions), - // store.subscribeAtom(atomConfig, callback) - subscribeAtom: (atomConfig) => - subscribeAtomWithStore(atomConfig, store, scopedOptions), - store, - }; + convertedOptions + ) as UseKeyValueApis), + // store.get() + ...(withStoreAndOptions( + atomsOfGet, + getGetIndex, + store, + convertedOptions + ) as GetKeyApis), + // store.useSet() + ...(withStoreAndOptions( + atomsOfUseSet, + getUseSetIndex, + store, + convertedOptions + ) as UseSetKeyApis), + // store.set(...args) + ...(withStoreAndOptions( + atomsOfSet, + getSetIndex, + store, + convertedOptions + ) as SetKeyApis), + // store.useState() + ...(withStoreAndOptions( + atomsOfUseState, + getUseStateIndex, + store, + convertedOptions + ) as UseKeyStateApis), + // store.subscribe(callback) + ...(withStoreAndOptions( + atomsOfSubscribe, + getSubscribeIndex, + store, + convertedOptions + ) as SubscribeKeyApis), + // store.useValue('key') + useValue: withKeyAndStoreAndOptions( + atomsOfUseValue, + store, + convertedOptions + ) as UseParamKeyValueApi, + // store.get('key') + get: withKeyAndStoreAndOptions( + atomsOfGet, + store, + convertedOptions + ) as GetParamKeyApi, + // store.useSet('key') + useSet: withKeyAndStoreAndOptions( + atomsOfUseSet, + store, + convertedOptions + ) as UseSetParamKeyApi, + // store.set('key', ...args) + set: withKeyAndStoreAndOptions( + atomsOfSet, + store, + convertedOptions + ) as SetParamKeyApi, + // store.useState('key') + useState: withKeyAndStoreAndOptions( + atomsOfUseState, + store, + convertedOptions + ) as UseParamKeyStateApi, + // store.subscribe('key', callback) + subscribe: withKeyAndStoreAndOptions( + atomsOfSubscribe, + store, + convertedOptions + ) as SubscribeParamKeyApi, + // store.useAtomValue(atomConfig) + useAtomValue: ((atomConfig, selector, equalityFnOrDeps, deps) => + // eslint-disable-next-line react-compiler/react-compiler + useAtomValueWithStore( + atomConfig, + store, + convertedOptions, + selector, + equalityFnOrDeps, + deps + )) as UseAtomParamValueApi, + // store.getAtom(atomConfig) + getAtom: (atomConfig) => + getAtomWithStore(atomConfig, store, convertedOptions), + // store.useSetAtom(atomConfig) + useSetAtom: (atomConfig) => + // eslint-disable-next-line react-compiler/react-compiler + useSetAtomWithStore(atomConfig, store, convertedOptions), + // store.setAtom(atomConfig, ...args) + setAtom: (atomConfig) => + setAtomWithStore(atomConfig, store, convertedOptions), + // store.useAtomState(atomConfig) + useAtomState: (atomConfig) => + // eslint-disable-next-line react-compiler/react-compiler + useAtomStateWithStore(atomConfig, store, convertedOptions), + // store.subscribeAtom(atomConfig, callback) + subscribeAtom: (atomConfig) => + subscribeAtomWithStore(atomConfig, store, convertedOptions), + store, + }), + [store, convertedOptions] + ); }; const useNameState = >(