Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/sweet-nails-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'jotai-x': patch
---

Add hooks `useStoreValue`, `useStoreSet`, `useStoreState`, `useStoreAtomValue`, `useStoreAtomSet`, `useStoreAtomState` to ease react-compiler eslint plugin complains
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
'./config/eslint/bases/regexp.cjs',
'./config/eslint/bases/jest.cjs',
'./config/eslint/bases/react.cjs',
'./config/eslint/bases/react-compiler.cjs',
'./config/eslint/bases/rtl.cjs',

'./config/eslint/bases/unicorn.cjs',
Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
- **`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 = useStoreValue('element');
// alternative
const element = store.useElementValue();
// alternative
const element = useElementStore().useValue('element');
Expand All @@ -78,27 +81,37 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
``` js
const store = useElementStore();

// Memoize the selector yourself
// Approach 1: Memoize the selector yourself
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
// Now it will only re-render if the uppercase value changes
const element = useStoreValue(store, 'element', toUpperCase);
// alternative
const element = store.useElementValue(toUpperCase);
// alternative
const element = useElementStore().useValue('element', toUpperCase);

// Pass an dependency array to prevent re-renders
// Approach 2: Pass an dependency array to prevent re-renders
const [n, setN] = useState(0); // n may change during re-renders
const numNthCharacter = useCallback((element) => element[n], [n]);
const numNthCharacter = useStoreValue(store, 'element', (element) => element[n], [n]);
// alternative
const numNthCharacter = store.useElementValue((element) => element[n], [n]);
// alternative
const numNthCharacter = store.useValue('element', (element) => element[n], [n]);
```
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
``` js
const store = useElementStore();
const element = useStoreSet(store, 'element');
// alternative
const element = store.useSetElement();
// alternative
const element = useElementStore().useSet('element');
```
- **`useState`**: Hooks for accessing and setting a state within a component, ensuring re-rendering when the state changes. See [useAtom](https://jotai.org/docs/core/use-atom).
``` js
const store = useElementStore();
const element = useStoreState(store, 'element');
// alternative
const element = store.useElementState();
// alternative
const element = useElementStore().useState('element');
Expand Down
6 changes: 6 additions & 0 deletions config/eslint/bases/react-compiler.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: ['react-compiler'],
rules: {
'react-compiler/react-compiler': 'error',
},
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-compiler": "19.0.0-beta-decd7b8-20250118",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-regexp": "^2.1.2",
"eslint-plugin-testing-library": "^6.2.0",
Expand Down
19 changes: 16 additions & 3 deletions packages/jotai-x/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
- **`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 = useStoreValue('element');
// alternative
const element = store.useElementValue();
// alternative
const element = useElementStore().useValue('element');
Expand All @@ -78,27 +81,37 @@ The **`createAtomStore`** function returns an object (**`AtomStoreApi`**) contai
``` js
const store = useElementStore();

// Memoize the selector yourself
// Approach 1: Memoize the selector yourself
const toUpperCase = useCallback((element) => element.toUpperCase(), []);
// Now it will only re-render if the uppercase value changes
const element = useStoreValue(store, 'element', toUpperCase);
// alternative
const element = store.useElementValue(toUpperCase);
// alternative
const element = useElementStore().useValue('element', toUpperCase);

// Pass an dependency array to prevent re-renders
// Approach 2: Pass an dependency array to prevent re-renders
const [n, setN] = useState(0); // n may change during re-renders
const numNthCharacter = useCallback((element) => element[n], [n]);
const numNthCharacter = useStoreValue(store, 'element', (element) => element[n], [n]);
// alternative
const numNthCharacter = store.useElementValue((element) => element[n], [n]);
// alternative
const numNthCharacter = store.useValue('element', (element) => element[n], [n]);
```
- **`useSet`**: Hooks for setting a state within a component. See [useSetAtom](https://jotai.org/docs/core/use-atom#usesetatom).
``` js
const store = useElementStore();
const element = useStoreSet(store, 'element');
// alternative
const element = store.useSetElement();
// alternative
const element = useElementStore().useSet('element');
```
- **`useState`**: Hooks for accessing and setting a state within a component, ensuring re-rendering when the state changes. See [useAtom](https://jotai.org/docs/core/use-atom).
``` js
const store = useElementStore();
const element = useStoreState(store, 'element');
// alternative
const element = store.useElementState();
// alternative
const element = useElementStore().useState('element');
Expand Down
121 changes: 118 additions & 3 deletions packages/jotai-x/src/createAtomStore.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
/* eslint-disable react-compiler/react-compiler */
import '@testing-library/jest-dom';

import React, { useCallback } from 'react';
import { act, queryByText, render, renderHook } from '@testing-library/react';
import { atom, PrimitiveAtom, useAtomValue } from 'jotai';
import { splitAtom } from 'jotai/utils';

import { createAtomStore } from './createAtomStore';
import {
createAtomStore,
useStoreAtomValue,
useStoreSet,
useStoreState,
useStoreValue,
} from './createAtomStore';

describe('createAtomStore', () => {
describe('no unnecessary rerender', () => {
Expand Down Expand Up @@ -403,6 +410,30 @@ describe('createAtomStore', () => {
);
};

const BecomeFriendUseValueNoReactCompilerComplain = () => {
// Just guarantee that the react compiler doesn't complain
/* eslint-enable react-compiler/react-compiler */
const store = useMyTestStoreStore();
const becomeFriends1 = useStoreValue(store, 'becomeFriends');
const becomeFriends2 = useStoreAtomValue(
store,
myTestStoreStore.atom.becomeFriends
);

return (
<button
type="button"
onClick={() => {
becomeFriends1();
becomeFriends2();
}}
>
Become Friends
</button>
);
/* eslint-disable react-compiler/react-compiler */
};

const BecomeFriendsGet = () => {
// Make sure both of these are actual functions, not wrapped functions
const store = useMyTestStoreStore();
Expand Down Expand Up @@ -472,6 +503,28 @@ describe('createAtomStore', () => {
);
};

const BecomeFriendsUseSetNoReactCompilerComplain = () => {
// Just guarantee that the react compiler doesn't complain
/* eslint-enable react-compiler/react-compiler */
const store = useMyTestStoreStore();
const setBecomeFriends = useStoreSet(store, 'becomeFriends');
const [becameFriends, setBecameFriends] = React.useState(false);

return (
<>
<button
type="button"
onClick={() => setBecomeFriends(() => setBecameFriends(true))}
>
Change Callback
</button>

<div>useSetBecameFriends: {becameFriends.toString()}</div>
</>
);
/* eslint-disable react-compiler/react-compiler */
};

const BecomeFriendsSet = () => {
const store = useMyTestStoreStore();
const [becameFriends, setBecameFriends] = React.useState(false);
Expand Down Expand Up @@ -546,9 +599,31 @@ describe('createAtomStore', () => {
);
};

const BecomeFriendsUseStateNoReactCompilerComplain = () => {
// Just guarantee that the react compiler doesn't complain
/* eslint-enable react-compiler/react-compiler */
const store = useMyTestStoreStore();
const [, setBecomeFriends] = useStoreState(store, 'becomeFriends');
const [becameFriends, setBecameFriends] = React.useState(false);

return (
<>
<button
type="button"
onClick={() => setBecomeFriends(() => setBecameFriends(true))}
>
Change Callback
</button>

<div>useBecameFriends: {becameFriends.toString()}</div>
</>
);
/* eslint-disable react-compiler/react-compiler */
};

beforeEach(() => {
renderHook(() => useMyTestStoreStore().useSetName()(INITIAL_NAME));
renderHook(() => useMyTestStoreStore().useSetAge()(INITIAL_AGE));
renderHook(() => useMyTestStoreStore().setName(INITIAL_NAME));
renderHook(() => useMyTestStoreStore().setAge(INITIAL_AGE));
});

it('passes default values from provider to consumer', () => {
Expand Down Expand Up @@ -797,6 +872,18 @@ describe('createAtomStore', () => {
expect(getByText('becameFriends: true')).toBeInTheDocument();
});

it('provides and useValue of functions with no react compiler complain', () => {
const { getByText } = render(
<BecomeFriendsProvider>
<BecomeFriendUseValueNoReactCompilerComplain />
</BecomeFriendsProvider>
);

expect(getByText('becameFriends: false')).toBeInTheDocument();
act(() => getByText('Become Friends').click());
expect(getByText('becameFriends: true')).toBeInTheDocument();
});

it('provides and get functions', () => {
const { getByText } = render(
<BecomeFriendsProvider>
Expand Down Expand Up @@ -849,6 +936,20 @@ describe('createAtomStore', () => {
expect(getByText('useSetBecameFriends: true')).toBeInTheDocument();
});

it('useSet of functions with no react compiler complain', () => {
const { getByText } = render(
<BecomeFriendsProvider>
<BecomeFriendsUseSetNoReactCompilerComplain />
<BecomeFriendsUseValueWithKeyParam />
</BecomeFriendsProvider>
);

act(() => getByText('Change Callback').click());
expect(getByText('useSetBecameFriends: false')).toBeInTheDocument();
act(() => getByText('Become Friends').click());
expect(getByText('useSetBecameFriends: true')).toBeInTheDocument();
});

it('set of functions', () => {
const { getByText } = render(
<BecomeFriendsProvider>
Expand Down Expand Up @@ -904,6 +1005,20 @@ describe('createAtomStore', () => {
act(() => getByText('Become Friends').click());
expect(getByText('useBecameFriends: true')).toBeInTheDocument();
});

it('use state functions with no react compiler complain', () => {
const { getByText } = render(
<BecomeFriendsProvider>
<BecomeFriendsUseStateNoReactCompilerComplain />
<BecomeFriendsUseValueWithKeyParam />
</BecomeFriendsProvider>
);

act(() => getByText('Change Callback').click());
expect(getByText('useBecameFriends: false')).toBeInTheDocument();
act(() => getByText('Become Friends').click());
expect(getByText('useBecameFriends: true')).toBeInTheDocument();
});
});

describe('scoped providers', () => {
Expand Down
Loading
Loading