Skip to content

Commit

Permalink
Merge pull request #22 from yf-yang/react-compiler
Browse files Browse the repository at this point in the history
React compiler
  • Loading branch information
zbeyens authored Jan 21, 2025
2 parents e5f4c61 + 3d36d42 commit 41e861a
Show file tree
Hide file tree
Showing 11 changed files with 690 additions and 41 deletions.
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

0 comments on commit 41e861a

Please sign in to comment.