From 4e3aa6b4a43eb4970e5b591cfa4b3952052e3f65 Mon Sep 17 00:00:00 2001 From: Yifei Date: Thu, 6 Feb 2025 15:26:07 +0800 Subject: [PATCH 1/2] fix: add tests --- .changeset/bright-coats-peel.md | 5 +++ README.md | 11 +++-- packages/jotai-x/README.md | 11 +++-- packages/jotai-x/src/createAtomStore.spec.tsx | 45 +++++++++++++++++-- packages/jotai-x/src/createAtomStore.ts | 14 +++--- 5 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 .changeset/bright-coats-peel.md diff --git a/.changeset/bright-coats-peel.md b/.changeset/bright-coats-peel.md new file mode 100644 index 0000000..8022cec --- /dev/null +++ b/.changeset/bright-coats-peel.md @@ -0,0 +1,5 @@ +--- +'jotai-x': patch +--- + +Add test cases and fix bugs diff --git a/README.md b/README.md index f2f580d..58e8f7b 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,7 @@ const [stars, setStars] = useAppState('stars'); // With selector and deps const upperName = useAppValue('name', { selector: (name) => name.toUpperCase(), - deps: [] -}); +}, []); ``` #### 2. Store Instance Methods @@ -204,14 +203,14 @@ const name = useAppValue('name'); // With selector const upperName = useAppValue('name', { selector: (name) => name.toUpperCase(), - deps: [] // Optional deps array -}); +}, [] // if selector is not memoized, provide deps array +); // With equality function const name = useAppValue('name', { selector: (name) => name, equalityFn: (prev, next) => prev.length === next.length -}); +}, []); ``` #### `useSet(key)` @@ -404,7 +403,7 @@ const selector = useCallback((name) => name.toUpperCase(), []); useUserValue('name', { selector }); // ✅ Correct - provide deps array -useUserValue('name', { selector: (name) => name.toUpperCase(), deps: [] }); +useUserValue('name', { selector: (name) => name.toUpperCase() }, []); // ✅ Correct - no selector useUserValue('name'); diff --git a/packages/jotai-x/README.md b/packages/jotai-x/README.md index f2f580d..58e8f7b 100644 --- a/packages/jotai-x/README.md +++ b/packages/jotai-x/README.md @@ -153,8 +153,7 @@ const [stars, setStars] = useAppState('stars'); // With selector and deps const upperName = useAppValue('name', { selector: (name) => name.toUpperCase(), - deps: [] -}); +}, []); ``` #### 2. Store Instance Methods @@ -204,14 +203,14 @@ const name = useAppValue('name'); // With selector const upperName = useAppValue('name', { selector: (name) => name.toUpperCase(), - deps: [] // Optional deps array -}); +}, [] // if selector is not memoized, provide deps array +); // With equality function const name = useAppValue('name', { selector: (name) => name, equalityFn: (prev, next) => prev.length === next.length -}); +}, []); ``` #### `useSet(key)` @@ -404,7 +403,7 @@ const selector = useCallback((name) => name.toUpperCase(), []); useUserValue('name', { selector }); // ✅ Correct - provide deps array -useUserValue('name', { selector: (name) => name.toUpperCase(), deps: [] }); +useUserValue('name', { selector: (name) => name.toUpperCase() }, []); // ✅ Correct - no selector useUserValue('name'); diff --git a/packages/jotai-x/src/createAtomStore.spec.tsx b/packages/jotai-x/src/createAtomStore.spec.tsx index 7f6af7c..f88b759 100644 --- a/packages/jotai-x/src/createAtomStore.spec.tsx +++ b/packages/jotai-x/src/createAtomStore.spec.tsx @@ -29,10 +29,8 @@ describe('createAtomStore', () => { arr: INITIAL_ARR, }; - const { useMyTestStoreStore, MyTestStoreProvider } = createAtomStore( - initialTestStoreValue, - { name: 'myTestStore' as const } - ); + const { useMyTestStoreStore, MyTestStoreProvider, useMyTestStoreValue } = + createAtomStore(initialTestStoreValue, { name: 'myTestStore' as const }); let numRenderCount = 0; const NumRenderer = () => { @@ -92,6 +90,24 @@ describe('createAtomStore', () => { ); }; + let arrNumRenderCountWithOneHook = 0; + const ArrNumRendererWithOneHook = () => { + arrNumRenderCountWithOneHook += 1; + const num = useMyTestStoreValue('num'); + const arrNum = useMyTestStoreValue( + 'arr', + { + selector: (v) => v[num], + }, + [num] + ); + return ( +
+
arrNumWithOneHook: {arrNum}
+
+ ); + }; + let arrNumRenderWithDepsCount = 0; const ArrNumRendererWithDeps = () => { arrNumRenderWithDepsCount += 1; @@ -110,6 +126,11 @@ describe('createAtomStore', () => { return
{arr0}
; }; + const BadSelectorRenderer2 = () => { + const arr0 = useMyTestStoreValue('arr', { selector: (v) => v[0] }); + return
{arr0}
; + }; + const Buttons = () => { const store = useMyTestStoreStore(); return ( @@ -154,6 +175,7 @@ describe('createAtomStore', () => { + @@ -166,6 +188,7 @@ describe('createAtomStore', () => { expect(arr0RenderCount).toBe(2); expect(arr1RenderCount).toBe(2); expect(arrNumRenderCount).toBe(2); + expect(arrNumRenderCountWithOneHook).toBe(2); expect(arrNumRenderWithDepsCount).toBe(2); expect(getByText('arrNum: alice')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: alice')).toBeInTheDocument(); @@ -177,6 +200,7 @@ describe('createAtomStore', () => { expect(arr0RenderCount).toBe(2); expect(arr1RenderCount).toBe(2); expect(arrNumRenderCount).toBe(5); + expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -188,6 +212,7 @@ describe('createAtomStore', () => { expect(arr0RenderCount).toBe(2); expect(arr1RenderCount).toBe(2); expect(arrNumRenderCount).toBe(5); + expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -199,6 +224,7 @@ describe('createAtomStore', () => { expect(arr0RenderCount).toBe(2); expect(arr1RenderCount).toBe(2); expect(arrNumRenderCount).toBe(5); + expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -210,6 +236,7 @@ describe('createAtomStore', () => { expect(arr0RenderCount).toBe(3); expect(arr1RenderCount).toBe(2); expect(arrNumRenderCount).toBe(8); + expect(arrNumRenderCountWithOneHook).toBe(8); expect(arrNumRenderWithDepsCount).toBe(8); expect(getByText('arrNum: ava')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: ava')).toBeInTheDocument(); @@ -224,6 +251,16 @@ describe('createAtomStore', () => { ) ).toThrow(); }); + + it('Throw error is user does memoize selector 2', () => { + expect(() => + render( + + + + ) + ).toThrow(); + }); }); describe('single provider', () => { diff --git a/packages/jotai-x/src/createAtomStore.ts b/packages/jotai-x/src/createAtomStore.ts index 9cdaac5..77f1e4a 100644 --- a/packages/jotai-x/src/createAtomStore.ts +++ b/packages/jotai-x/src/createAtomStore.ts @@ -2,6 +2,8 @@ import React from 'react'; import { getDefaultStore, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { selectAtom, useHydrateAtoms } from 'jotai/utils'; + + import { atomWithFn } from './atomWithFn'; import { createAtomProvider, useAtomStore } from './createAtomProvider'; @@ -493,10 +495,10 @@ export interface CreateAtomStoreOptions< * Each property will have a getter and setter. * * @example - * const { exampleStore, useExampleStore } = createAtomStore({ count: 1, say: 'hello' }, { name: 'example' as const }) - * const [count, setCount] = useExampleStore().useCountState() - * const say = useExampleStore().useSayValue() - * const setSay = useExampleStore().useSetSay() + * const { exampleStore, useExampleStore, useExampleValue, useExampleState, useExampleSet } = createAtomStore({ count: 1, say: 'hello' }, { name: 'example' as const }) + * const [count, setCount] = useExampleState() + * const say = useExampleValue('say') + * const setSay = useExampleSet('say') * setSay('world') * const countAtom = exampleStore.atom.count */ @@ -591,7 +593,7 @@ export const createAtomStore = < if (renderCount > infiniteRenderDetectionLimit) { throw new Error( ` -useValue/useValue has rendered ${infiniteRenderDetectionLimit} times in the same render. +useValue/useValue/useValue has rendered ${infiniteRenderDetectionLimit} times in the same render. It is very likely to have fallen into an infinite loop. That is because you do not memoize the selector/equalityFn function param. Please wrap them with useCallback or configure the deps array correctly.` @@ -1008,4 +1010,4 @@ export function useStoreAtomState( atom: WritableAtom ) { return store.useAtomState(atom); -} +} \ No newline at end of file From 6643d0279b7f9afe3d0b3958e665d45c1f50bdf4 Mon Sep 17 00:00:00 2001 From: Yifei Date: Thu, 6 Feb 2025 15:36:16 +0800 Subject: [PATCH 2/2] fix: bug fix --- packages/jotai-x/src/createAtomStore.spec.tsx | 31 +++++++++++++++++-- packages/jotai-x/src/createAtomStore.ts | 8 ++--- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/packages/jotai-x/src/createAtomStore.spec.tsx b/packages/jotai-x/src/createAtomStore.spec.tsx index f88b759..c0feb16 100644 --- a/packages/jotai-x/src/createAtomStore.spec.tsx +++ b/packages/jotai-x/src/createAtomStore.spec.tsx @@ -29,8 +29,14 @@ describe('createAtomStore', () => { arr: INITIAL_ARR, }; - const { useMyTestStoreStore, MyTestStoreProvider, useMyTestStoreValue } = - createAtomStore(initialTestStoreValue, { name: 'myTestStore' as const }); + const { + myTestStoreStore, + useMyTestStoreStore, + MyTestStoreProvider, + useMyTestStoreValue, + } = createAtomStore(initialTestStoreValue, { + name: 'myTestStore' as const, + }); let numRenderCount = 0; const NumRenderer = () => { @@ -121,6 +127,21 @@ describe('createAtomStore', () => { ); }; + let arrNumRenderWithDepsAndAtomCount = 0; + const ArrNumRendererWithDepsAndAtom = () => { + arrNumRenderWithDepsAndAtomCount += 1; + const store = useMyTestStoreStore(); + const numAtom = myTestStoreStore.atom.num; + const num = store.useAtomValue(numAtom); + const arrAtom = myTestStoreStore.atom.arr; + const arrNum = store.useAtomValue(arrAtom, (v) => v[num], [num]); + return ( +
+
arrNumWithDepsAndAtom: {arrNum}
+
+ ); + }; + const BadSelectorRenderer = () => { const arr0 = useMyTestStoreStore().useArrValue((v) => v[0]); return
{arr0}
; @@ -177,6 +198,7 @@ describe('createAtomStore', () => { + ); @@ -190,6 +212,7 @@ describe('createAtomStore', () => { expect(arrNumRenderCount).toBe(2); expect(arrNumRenderCountWithOneHook).toBe(2); expect(arrNumRenderWithDepsCount).toBe(2); + expect(arrNumRenderWithDepsAndAtomCount).toBe(2); expect(getByText('arrNum: alice')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: alice')).toBeInTheDocument(); @@ -202,6 +225,7 @@ describe('createAtomStore', () => { expect(arrNumRenderCount).toBe(5); expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); + expect(arrNumRenderWithDepsAndAtomCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -214,6 +238,7 @@ describe('createAtomStore', () => { expect(arrNumRenderCount).toBe(5); expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); + expect(arrNumRenderWithDepsAndAtomCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -226,6 +251,7 @@ describe('createAtomStore', () => { expect(arrNumRenderCount).toBe(5); expect(arrNumRenderCountWithOneHook).toBe(5); expect(arrNumRenderWithDepsCount).toBe(5); + expect(arrNumRenderWithDepsAndAtomCount).toBe(5); expect(getByText('arrNum: bob')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument(); @@ -238,6 +264,7 @@ describe('createAtomStore', () => { expect(arrNumRenderCount).toBe(8); expect(arrNumRenderCountWithOneHook).toBe(8); expect(arrNumRenderWithDepsCount).toBe(8); + expect(arrNumRenderWithDepsAndAtomCount).toBe(8); expect(getByText('arrNum: ava')).toBeInTheDocument(); expect(getByText('arrNumWithDeps: ava')).toBeInTheDocument(); }); diff --git a/packages/jotai-x/src/createAtomStore.ts b/packages/jotai-x/src/createAtomStore.ts index 77f1e4a..f1108ef 100644 --- a/packages/jotai-x/src/createAtomStore.ts +++ b/packages/jotai-x/src/createAtomStore.ts @@ -2,8 +2,6 @@ import React from 'react'; import { getDefaultStore, useAtom, useAtomValue, useSetAtom } from 'jotai'; import { selectAtom, useHydrateAtoms } from 'jotai/utils'; - - import { atomWithFn } from './atomWithFn'; import { createAtomProvider, useAtomStore } from './createAtomProvider'; @@ -900,8 +898,8 @@ Please wrap them with useCallback or configure the deps array correctly.` store, options, selector as any, - equalityFn as any, - deps + equalityFn ?? deps, + equalityFn && deps ); }; @@ -1010,4 +1008,4 @@ export function useStoreAtomState( atom: WritableAtom ) { return store.useAtomState(atom); -} \ No newline at end of file +}