Skip to content

Commit 076596a

Browse files
committed
feat: deal with memoization issues
1 parent db6bcb3 commit 076596a

File tree

4 files changed

+259
-39
lines changed

4 files changed

+259
-39
lines changed

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ The **`options`** object can include several properties to customize the behavio
5959
- **`delay`**: If you need to introduce a delay in state updates, you can specify it here. Optional.
6060
- **`effect`**: A React component that can be used to run effects inside the provider. Optional.
6161
- **`extend`**: Extend the store with derived atoms based on the store state. Optional.
62+
- **`infiniteRenderDetectionLimit`**: In non production mode, it will throw an error if the number of `useValue` hook calls exceeds this limit during the same render. Optional.
6263
6364
#### Return Value
6465
@@ -73,14 +74,20 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
7374
// alternative
7475
const element = useElementStore().useValue('element');
7576
```
76-
- Advanced: `useValue` supports parameters `selector`, which is a function that takes the current value and returns a new value and parameter `equalityFn`, which is a function that compares the previous and new values and only re-renders if they are not equal. Internally, it uses [selectAtom](https://jotai.org/docs/utilities/select#selectatom)
77+
- Advanced: `useValue` supports parameters `selector`, which is a function that takes the current value and returns a new value and parameter `equalityFn`, which is a function that compares the previous and new values and only re-renders if they are not equal. Internally, it uses [selectAtom](https://jotai.org/docs/utilities/select#selectatom). You must memoize `selector`/`equalityFn` adequately.
7778
``` js
7879
const store = useElementStore();
80+
81+
// Memoize the selector yourself
7982
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
8083
// Now it will only re-render if the uppercase value changes
8184
const element = store.useElementValue(toUpperCase);
8285
// alternative
8386
const element = useElementStore().useValue('element', toUpperCase);
87+
88+
// Pass an dependency array to prevent re-renders
89+
const [n, setN] = useState(0); // n may change during re-renders
90+
const numNthCharacter = useCallback((element) => element[n], [n]);
8491
```
8592
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
8693
``` js
@@ -296,7 +303,32 @@ const Component = () => {
296303
};
297304
```
298305

299-
## Migrate from v1 to v2
306+
## **Troubleshooting**
307+
### Error: use<Key>Value/useValue has rendered `num` times in the same render
308+
When calling `use<Key>Value` or `useValue` with `selector` and `equalityFn`, those two functions must be memoized. Otherwise, the component will re-render infinitely. In order to prevent developers from making this mistake, in non-production mode (`process.env.NODE_ENV !== 'production'`), we will throw an error if the number of `useValue` hook calls exceeds a certain limit.
309+
310+
Usually, this error is caused by not memoizing the `selector` or `equalityFn` functions. To fix this, you can use `useCallback` to memoize the functions, or pass a dependency array yourselves. We support multiple alternatives:
311+
312+
```tsx
313+
// No selector at all
314+
useValue('key')
315+
316+
// Memoize with useCallback yourself
317+
const memoizedSelector = useCallback(selector, [...]);
318+
const memoizedEqualityFn = useCallback(equalityFn, [...]);
319+
// memoizedEqualityFn is an optional parameter
320+
useValue('key', memoizedSelector, memoizedEqualityFn);
321+
322+
// Provide selector and its deps
323+
useValue('key', selector, [...]);
324+
325+
// Provide selector and equalityFn and all of their deps
326+
useValue('key', selector, equalityFn, [...]);
327+
```
328+
329+
The error could also be a false positive, since the internal counter is shared across all `useValue` calls of the same store. If your component tree is very deep and uses the same store's `useValue` multiple times, then the limit could be reached. To deal with that, `createAtomStore` supports an optional parameter `infiniteRenderDetectionLimit`. You can configure that with a higher limit.
330+
331+
## **Migrate from v1 to v2**
300332

301333
1. Return of `use<Name>Store`: `get` is renamed to `use<Key>Value`, `set` is renamed to `useSet<Key>`, `use` is renamed to `useState`.
302334
``` diff

packages/jotai-x/README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ The **`options`** object can include several properties to customize the behavio
5959
- **`delay`**: If you need to introduce a delay in state updates, you can specify it here. Optional.
6060
- **`effect`**: A React component that can be used to run effects inside the provider. Optional.
6161
- **`extend`**: Extend the store with derived atoms based on the store state. Optional.
62+
- **`infiniteRenderDetectionLimit`**: In non production mode, it will throw an error if the number of `useValue` hook calls exceeds this limit during the same render. Optional.
6263
6364
#### Return Value
6465
@@ -73,14 +74,20 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
7374
// alternative
7475
const element = useElementStore().useValue('element');
7576
```
76-
- Advanced: `useValue` supports parameters `selector`, which is a function that takes the current value and returns a new value and parameter `equalityFn`, which is a function that compares the previous and new values and only re-renders if they are not equal. Internally, it uses [selectAtom](https://jotai.org/docs/utilities/select#selectatom)
77+
- Advanced: `useValue` supports parameters `selector`, which is a function that takes the current value and returns a new value and parameter `equalityFn`, which is a function that compares the previous and new values and only re-renders if they are not equal. Internally, it uses [selectAtom](https://jotai.org/docs/utilities/select#selectatom). You must memoize `selector`/`equalityFn` adequately.
7778
``` js
7879
const store = useElementStore();
80+
81+
// Memoize the selector yourself
7982
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
8083
// Now it will only re-render if the uppercase value changes
8184
const element = store.useElementValue(toUpperCase);
8285
// alternative
8386
const element = useElementStore().useValue('element', toUpperCase);
87+
88+
// Pass an dependency array to prevent re-renders
89+
const [n, setN] = useState(0); // n may change during re-renders
90+
const numNthCharacter = useCallback((element) => element[n], [n]);
8491
```
8592
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
8693
``` js
@@ -296,7 +303,32 @@ const Component = () => {
296303
};
297304
```
298305

299-
## Migrate from v1 to v2
306+
## **Troubleshooting**
307+
### Error: use<Key>Value/useValue has rendered `num` times in the same render
308+
When calling `use<Key>Value` or `useValue` with `selector` and `equalityFn`, those two functions must be memoized. Otherwise, the component will re-render infinitely. In order to prevent developers from making this mistake, in non-production mode (`process.env.NODE_ENV !== 'production'`), we will throw an error if the number of `useValue` hook calls exceeds a certain limit.
309+
310+
Usually, this error is caused by not memoizing the `selector` or `equalityFn` functions. To fix this, you can use `useCallback` to memoize the functions, or pass a dependency array yourselves. We support multiple alternatives:
311+
312+
```tsx
313+
// No selector at all
314+
useValue('key')
315+
316+
// Memoize with useCallback yourself
317+
const memoizedSelector = useCallback(selector, [...]);
318+
const memoizedEqualityFn = useCallback(equalityFn, [...]);
319+
// memoizedEqualityFn is an optional parameter
320+
useValue('key', memoizedSelector, memoizedEqualityFn);
321+
322+
// Provide selector and its deps
323+
useValue('key', selector, [...]);
324+
325+
// Provide selector and equalityFn and all of their deps
326+
useValue('key', selector, equalityFn, [...]);
327+
```
328+
329+
The error could also be a false positive, since the internal counter is shared across all `useValue` calls of the same store. If your component tree is very deep and uses the same store's `useValue` multiple times, then the limit could be reached. To deal with that, `createAtomStore` supports an optional parameter `infiniteRenderDetectionLimit`. You can configure that with a higher limit.
330+
331+
## **Migrate from v1 to v2**
300332

301333
1. Return of `use<Name>Store`: `get` is renamed to `use<Key>Value`, `set` is renamed to `useSet<Key>`, `use` is renamed to `useState`.
302334
``` diff

packages/jotai-x/src/createAtomStore.spec.tsx

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe('createAtomStore', () => {
1414
arr: string[];
1515
};
1616

17-
const INITIAL_NUM = 42;
17+
const INITIAL_NUM = 0;
1818
const INITIAL_ARR = ['alice', 'bob'];
1919

2020
const initialTestStoreValue: MyTestStoreValue = {
@@ -71,6 +71,38 @@ describe('createAtomStore', () => {
7171
return <div>{arr1}</div>;
7272
};
7373

74+
let arrNumRenderCount = 0;
75+
const ArrNumRenderer = () => {
76+
arrNumRenderCount += 1;
77+
const store = useMyTestStoreStore();
78+
const num = store.useNumValue();
79+
const selectArrNum = useCallback((v: string[]) => v[num], [num]);
80+
const arrNum = store.useArrValue(selectArrNum);
81+
return (
82+
<div>
83+
<div>arrNum: {arrNum}</div>
84+
</div>
85+
);
86+
};
87+
88+
let arrNumRenderWithDepsCount = 0;
89+
const ArrNumRendererWithDeps = () => {
90+
arrNumRenderWithDepsCount += 1;
91+
const store = useMyTestStoreStore();
92+
const num = store.useNumValue();
93+
const arrNum = store.useArrValue((v) => v[num], [num]);
94+
return (
95+
<div>
96+
<div>arrNumWithDeps: {arrNum}</div>
97+
</div>
98+
);
99+
};
100+
101+
const BadSelectorRenderer = () => {
102+
const arr0 = useMyTestStoreStore().useArrValue((v) => v[0]);
103+
return <div>{arr0}</div>;
104+
};
105+
74106
const Buttons = () => {
75107
const store = useMyTestStoreStore();
76108
return (
@@ -95,7 +127,10 @@ describe('createAtomStore', () => {
95127
</button>
96128
<button
97129
type="button"
98-
onClick={() => store.setArr(['ava', ...store.getArr().slice(1)])}
130+
onClick={() => {
131+
store.setArr(['ava', ...store.getArr().slice(1)]);
132+
store.setNum(0);
133+
}}
99134
>
100135
modify arr0
101136
</button>
@@ -111,6 +146,8 @@ describe('createAtomStore', () => {
111146
<ArrRendererWithShallow />
112147
<Arr0Renderer />
113148
<Arr1Renderer />
149+
<ArrNumRenderer />
150+
<ArrNumRendererWithDeps />
114151
<Buttons />
115152
</MyTestStoreProvider>
116153
);
@@ -121,34 +158,64 @@ describe('createAtomStore', () => {
121158
expect(arrRendererWithShallowRenderCount).toBe(2);
122159
expect(arr0RenderCount).toBe(2);
123160
expect(arr1RenderCount).toBe(2);
161+
expect(arrNumRenderCount).toBe(2);
162+
expect(arrNumRenderWithDepsCount).toBe(2);
163+
expect(getByText('arrNum: alice')).toBeInTheDocument();
164+
expect(getByText('arrNumWithDeps: alice')).toBeInTheDocument();
124165

125166
act(() => getByText('increment').click());
126167
expect(numRenderCount).toBe(3);
127168
expect(arrRenderCount).toBe(2);
128169
expect(arrRendererWithShallowRenderCount).toBe(2);
129170
expect(arr0RenderCount).toBe(2);
130171
expect(arr1RenderCount).toBe(2);
172+
expect(arrNumRenderCount).toBe(5);
173+
expect(arrNumRenderWithDepsCount).toBe(5);
174+
expect(getByText('arrNum: bob')).toBeInTheDocument();
175+
expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument();
131176

132177
act(() => getByText('add one name').click());
133178
expect(numRenderCount).toBe(3);
134179
expect(arrRenderCount).toBe(3);
135180
expect(arrRendererWithShallowRenderCount).toBe(3);
136181
expect(arr0RenderCount).toBe(2);
137182
expect(arr1RenderCount).toBe(2);
183+
expect(arrNumRenderCount).toBe(5);
184+
expect(arrNumRenderWithDepsCount).toBe(5);
185+
expect(getByText('arrNum: bob')).toBeInTheDocument();
186+
expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument();
138187

139188
act(() => getByText('copy array').click());
140189
expect(numRenderCount).toBe(3);
141190
expect(arrRenderCount).toBe(4);
142191
expect(arrRendererWithShallowRenderCount).toBe(3);
143192
expect(arr0RenderCount).toBe(2);
144193
expect(arr1RenderCount).toBe(2);
194+
expect(arrNumRenderCount).toBe(5);
195+
expect(arrNumRenderWithDepsCount).toBe(5);
196+
expect(getByText('arrNum: bob')).toBeInTheDocument();
197+
expect(getByText('arrNumWithDeps: bob')).toBeInTheDocument();
145198

146199
act(() => getByText('modify arr0').click());
147-
expect(numRenderCount).toBe(3);
200+
expect(numRenderCount).toBe(4);
148201
expect(arrRenderCount).toBe(5);
149202
expect(arrRendererWithShallowRenderCount).toBe(4);
150203
expect(arr0RenderCount).toBe(3);
151204
expect(arr1RenderCount).toBe(2);
205+
expect(arrNumRenderCount).toBe(8);
206+
expect(arrNumRenderWithDepsCount).toBe(8);
207+
expect(getByText('arrNum: ava')).toBeInTheDocument();
208+
expect(getByText('arrNumWithDeps: ava')).toBeInTheDocument();
209+
});
210+
211+
it('Throw error if user does not memoize selector', () => {
212+
expect(() =>
213+
render(
214+
<MyTestStoreProvider>
215+
<BadSelectorRenderer />
216+
</MyTestStoreProvider>
217+
)
218+
).toThrow();
152219
});
153220
});
154221

0 commit comments

Comments
 (0)