Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 5 additions & 0 deletions .changeset/silent-bugs-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'jotai-x': minor
---

Add alternative selector and equalityFn support to `useValue`
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,22 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai

- **`use<Name>Store`**:
- A function that returns the following objects: **`useValue`**, **`useSet`**, **`useState`**, where values are hooks for each state defined in the store, and **`get`**, **`set`**, **`subscribe`**, **`store`**, where values are direct get/set accessors to modify each state.
- **`useValue`**: Hooks for accessing a state within a component, ensuring re-rendering when the state changes. See [useAtomValue](https://jotai.org/docs/core/use-atom#useatomvalue).
- **`useValue`**: Hooks for accessing a state within a component, ensuring re-rendering when the state changes. See [useAtomValue](https://jotai.org/docs/core/use-atom#useatomvalue).
``` js
const store = useElementStore();
const element = store.useElementValue();
// alternative
const element = useElementStore().useValue('element');
```
- 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)
``` js
const store = useElementStore();
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
// Now it will only re-render if the uppercase value changes
const element = store.useElementValue(toUpperCase);
// alternative
const element = useElementStore().useValue('element', toUpperCase);
```
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
``` js
const store = useElementStore();
Expand Down
11 changes: 10 additions & 1 deletion packages/jotai-x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,22 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai

- **`use<Name>Store`**:
- A function that returns the following objects: **`useValue`**, **`useSet`**, **`useState`**, where values are hooks for each state defined in the store, and **`get`**, **`set`**, **`subscribe`**, **`store`**, where values are direct get/set accessors to modify each state.
- **`useValue`**: Hooks for accessing a state within a component, ensuring re-rendering when the state changes. See [useAtomValue](https://jotai.org/docs/core/use-atom#useatomvalue).
- **`useValue`**: Hooks for accessing a state within a component, ensuring re-rendering when the state changes. See [useAtomValue](https://jotai.org/docs/core/use-atom#useatomvalue).
``` js
const store = useElementStore();
const element = store.useElementValue();
// alternative
const element = useElementStore().useValue('element');
```
- 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)
``` js
const store = useElementStore();
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
// Now it will only re-render if the uppercase value changes
const element = store.useElementValue(toUpperCase);
// alternative
const element = useElementStore().useValue('element', toUpperCase);
```
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
``` js
const store = useElementStore();
Expand Down
3 changes: 3 additions & 0 deletions packages/jotai-x/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@
],
"publishConfig": {
"access": "public"
},
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.0"
}
}
142 changes: 142 additions & 0 deletions packages/jotai-x/src/createAtomStore.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,148 @@ import { splitAtom } from 'jotai/utils';
import { createAtomStore } from './createAtomStore';

describe('createAtomStore', () => {
describe('no unnecessary rerender', () => {
type MyTestStoreValue = {
num: number;
arr: string[];
};

const INITIAL_NUM = 42;
const INITIAL_ARR = ['alice', 'bob'];

const initialTestStoreValue: MyTestStoreValue = {
num: INITIAL_NUM,
arr: INITIAL_ARR,
};

const { useMyTestStoreStore, MyTestStoreProvider } = createAtomStore(
initialTestStoreValue,
{ name: 'myTestStore' as const }
);

let numRenderCount = 0;
const NumRenderer = () => {
numRenderCount += 1;
const num = useMyTestStoreStore().useNumValue();
return <div>{num}</div>;
};

let arrRenderCount = 0;
const ArrRenderer = () => {
arrRenderCount += 1;
const arr = useMyTestStoreStore().useArrValue();
return <div>{`[${arr.join(', ')}]`}</div>;
};

let arrRendererWithShallowRenderCount = 0;
const ArrRendererWithShallow = () => {
arrRendererWithShallowRenderCount += 1;
const equalityFn = (a: string[], b: string[]) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i += 1) {
if (a[i] !== b[i]) return false;
}
return true;
};
const arr = useMyTestStoreStore().useArrValue(undefined, equalityFn);
return <div>{`[${arr.join(', ')}]`}</div>;
};

let arr0RenderCount = 0;
const Arr0Renderer = () => {
arr0RenderCount += 1;
const arr0 = useMyTestStoreStore().useArrValue((v) => v[0]);
return <div>{arr0}</div>;
};

let arr1RenderCount = 0;
const Arr1Renderer = () => {
arr1RenderCount += 1;
const arr1 = useMyTestStoreStore().useArrValue((v) => v[1]);
return <div>{arr1}</div>;
};

const Buttons = () => {
const store = useMyTestStoreStore();
return (
<div>
<button
type="button"
onClick={() => store.setNum(store.getNum() + 1)}
>
increment
</button>
<button
type="button"
onClick={() => store.setArr([...store.getArr(), 'charlie'])}
>
add one name
</button>
<button
type="button"
onClick={() => store.setArr([...store.getArr()])}
>
copy array
</button>
<button
type="button"
onClick={() => store.setArr(['ava', ...store.getArr().slice(1)])}
>
modify arr0
</button>
</div>
);
};

it('does not rerender when unrelated state changes', () => {
const { getByText } = render(
<MyTestStoreProvider>
<NumRenderer />
<ArrRenderer />
<ArrRendererWithShallow />
<Arr0Renderer />
<Arr1Renderer />
<Buttons />
</MyTestStoreProvider>
);

// Why it's 2, not 1? Is React StrictMode causing this?
expect(numRenderCount).toBe(2);
expect(arrRenderCount).toBe(2);
expect(arrRendererWithShallowRenderCount).toBe(2);
expect(arr0RenderCount).toBe(2);
expect(arr1RenderCount).toBe(2);

act(() => getByText('increment').click());
expect(numRenderCount).toBe(3);
expect(arrRenderCount).toBe(2);
expect(arrRendererWithShallowRenderCount).toBe(2);
expect(arr0RenderCount).toBe(2);
expect(arr1RenderCount).toBe(2);

act(() => getByText('add one name').click());
expect(numRenderCount).toBe(3);
expect(arrRenderCount).toBe(3);
expect(arrRendererWithShallowRenderCount).toBe(3);
expect(arr0RenderCount).toBe(2);
expect(arr1RenderCount).toBe(2);

act(() => getByText('copy array').click());
expect(numRenderCount).toBe(3);
expect(arrRenderCount).toBe(4);
expect(arrRendererWithShallowRenderCount).toBe(3);
expect(arr0RenderCount).toBe(2);
expect(arr1RenderCount).toBe(2);

act(() => getByText('modify arr0').click());
expect(numRenderCount).toBe(3);
expect(arrRenderCount).toBe(5);
expect(arrRendererWithShallowRenderCount).toBe(4);
expect(arr0RenderCount).toBe(3);
expect(arr1RenderCount).toBe(2);
});
});

describe('single provider', () => {
type MyTestStoreValue = {
name: string;
Expand Down
Loading