Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid rerenders if atom value doesn't change #1158

Closed
tobias-walle opened this issue May 14, 2022 · 24 comments · Fixed by #1159
Closed

Avoid rerenders if atom value doesn't change #1158

tobias-walle opened this issue May 14, 2022 · 24 comments · Fixed by #1159

Comments

@tobias-walle
Copy link
Contributor

Currently a React Component that uses an readonly atom, gets always rerendered if one of the atoms dependencies changes. Even if the value of the atom is the same.

I created an example to demonstrate the issue:
https://codesandbox.io/s/jotai-rerender-demo-pgr5ep?file=/src/App.tsx
If you click the button, ComponentA is always rendered, even if its atom value never changes.

This behaviour causes performance issues in one of my apps, which has a very big state (stateAtom) that comes from the server and is therefore saved in one atom, and heavily uses selectAtom to split the big state into smaller atoms. Everytime the stateAtom is updated, most of the App gets rerendered, even if the state change affects just one component.

This sandbox demonstrates the same issue with selectAtom:
https://codesandbox.io/s/jotai-rerender-demo-select-m3umz7

I expected that a component would only update if the value of the atom changes.

Is there a way to prevent this issue?

@AjaxSolutions
Copy link
Collaborator

AjaxSolutions commented May 14, 2022

I made a small change. I find it interesting that CounterComponent re-renders even though count is not changed because it's not increased. Just calling setCount with the same value causes a re-render.

const CounterComponent: FC = () => {
  console.log("RENDER Counter");
  const [count, setCount] = useAtom(countAtom);
  return (
    <button onClick={() => setCount((c) => c)}>Click me ({count})</button>
  );
};

@dai-shi
Copy link
Member

dai-shi commented May 15, 2022

This is something being asked several times. #1015 #1137 #1155
See how it behaves: https://codesandbox.io/s/jotai-rerender-demo-forked-clne10

This behaviour causes performance issues in one of my apps

Now, this is a real problem. Thanks for sharing. My assumption is there are some heavy computations in a render function. Can you guess what they are? If so, wrapping with useMemo solves the problem, doesn't it?

Is there a way to prevent this issue?

For now, I don't have a solution, but maybe spend some time to see if there are any better solutions.

@dai-shi
Copy link
Member

dai-shi commented May 15, 2022

Just calling setCount with the same value causes a re-render.

Yeah, this is more counter-intuitive than derived atom cases.
This one would especially be nice to fix.

@dai-shi
Copy link
Member

dai-shi commented May 15, 2022

Okay, what's news to me is that React changes useReducer behavior with 18 (createRoot) and 17.

https://codesandbox.io/s/eager-lake-khw3pw?file=/src/App.js

@dai-shi
Copy link
Member

dai-shi commented May 15, 2022

#1159 should fix the same value with non-derived atoms #1158 (comment).

For derived atoms as originally reported, this is working as expected.
The rationale is that the library doesn't know how heavy read function is.
So, we run it in "render" phase and then "bail out" if value isn't changed.
This is same as how useReducer works (especially in React 18).

The solution for heavy computation is useMemo #1158 (comment) or putting the computation in derived atoms.

Closing this but I would love to hear the feedbacks.

@tobias-walle
Copy link
Contributor Author

@dai-shi Thank you for the clarification! I didn't now about the difference between the render and the commit phase.

I was able to find the source of my performance issues with the React profiler and a useMemo (as recommended).

Off topic: Thank you for your great work on this library. I really love using it!

@Grafikart
Copy link

Grafikart commented Dec 9, 2022

It could be interesting to add the information on the documentation regarding this issue. In recoil a re-render happens only if the derived value changed whereas in Jotai a re-render happens every time the source atom changed.

For instance in the part "Composing atoms"

export const textAtom = atom('hello')
export const textLenAtom = atom((get) => get(textAtom).length)

Even if the atom length doesn't change (but the text does "hello" becomes "hi !!") it will cause a rerender

function MyComponent () {
    const length = useAtomValue(textLenAtom); // Wil rerender when textAtom change no matter the result of textLenAtom
    return <div>Text length : {length}</div>
}

@dai-shi
Copy link
Member

dai-shi commented Dec 10, 2022

This behavior is intentional but not super understandable. I think it's described in the core api doc.
We will change the behavior in Jotai v2 API, which is closer to Recoil in this sense.
Links about Jotai v2 API: https://twitter.com/dai_shi/status/1601092344262316033

@tmaximini
Copy link

tmaximini commented Jun 1, 2023

@dai-shi This is an old issue but I am wondering if there is any solution for the "do not re-render derived atoms when the value is same" problem.
We're making heavy use of derived aroms throughout our app, and it's causing tons of unnecessary re-renders.
Has this behavior changed in 2.x? Or would it still re-render. (We can't upgrade yet since we're still on React 16)

And I don't understand how useMemo is supposed to solve that issue, since the values come from useAtom and that can't be used inside useMemo. (React Hook "useAtom" cannot be called inside a callback)

It would be nice if there is a way to access that value that only re-renders if the value changes, e.g. via useAtomValue

@dai-shi
Copy link
Member

dai-shi commented Jun 1, 2023

This behavior is changed in v2. It's one of the reasons we introduced v2.
BTW, for now, v2 unofficially supports the latest version of React 16.

@wootra
Copy link
Collaborator

wootra commented Jun 2, 2023

It could be interesting to add the information on the documentation regarding this issue. In recoil a re-render happens only if the derived value changed whereas in Jotai a re-render happens every time the source atom changed.

For instance in the part "Composing atoms"

export const textAtom = atom('hello')
export const textLenAtom = atom((get) => get(textAtom).length)

Even if the atom length doesn't change (but the text does "hello" becomes "hi !!") it will cause a rerender

function MyComponent () {
    const length = useAtomValue(textLenAtom); // Wil rerender when textAtom change no matter the result of textLenAtom
    return <div>Text length : {length}</div>
}

I tried with with the similar example, and cannot see derived atom renders when the value is changed ( after dai-shi's update).
here is my test:

{
  "name": "reducer-test",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },
  "dependencies": {
    "jotai": "^2.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.0.37",
    "@types/react-dom": "^18.0.11",
    "@vitejs/plugin-react": "^4.0.0",
    "eslint": "^8.38.0",
    "eslint-plugin-react": "^7.32.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.3.4",
    "vite": "^4.3.9"
  }
}

and test code:

import { atom } from 'jotai/vanilla';
import { useAtom, useAtomValue } from 'jotai/react';
import './App.css';
import React from 'react';
const anAtom = atom(0);
const derivedAtom = atom(get => get(anAtom) + ' derived');
const derivedAtom2 = atom(get => get(derivedAtom).length);
function App() {
    const [count, setCount] = useAtom(anAtom);
    console.log('in render', count);
    return (
        <>
            <div className='card'>
                <button onClick={() => setCount(count + 1)}>
                    count is {count}
                    <Child />
                </button>
            </div>
        </>
    );
}
const Child = React.memo(() => {
    console.log('in Child');
    const count = useAtomValue(derivedAtom2);
    return <div>{count}</div>;
});
Child.displayName = 'Child';

export default App;

maybe the re-rendering is because of react version? I didn't test with react 16.

@joacub
Copy link

joacub commented Jun 13, 2023

this approach in the new version 2.2.0 is not working any more, is causing a infinite loop

const state = useAtomValue(selectAtom(rootAtom, getValues));

@dai-shi
Copy link
Member

dai-shi commented Jun 13, 2023

Is getValues stable? Does it work in the older versions?

@wootra
Copy link
Collaborator

wootra commented Jun 13, 2023

this approach in the new version 2.2.0 is not working any more, is causing a infinite loop

const state = useAtomValue(selectAtom(rootAtom, getValues));

I think this code itself create infinite loop since when you call selectAtom, it create a new atom triggering useAtomValue to rerender.

@joacub
Copy link

joacub commented Jun 13, 2023

I have memorized the selector , it was working before as per documentation says using a callback memorizing the resulting selector

@dai-shi
Copy link
Member

dai-shi commented Jun 13, 2023

Can you identify which version causes the issue at first?

@joacub
Copy link

joacub commented Jun 13, 2023

it was working in the version 2.1.1, and I updated now to 2.2.0 and start doing the infinite loop. maybe something with the new implementation of async states it maybe is detecting an async function new over and over, I dint investigate any thing im just guessing

@dai-shi
Copy link
Member

dai-shi commented Jun 14, 2023

That's sounds unexpected, because nothing around async behavior is changed since v2.1.1.

Would you be able to create a minimal reproduction that works in v2.1.1 and fails in v2.2.0 with https://csb.jotai.org ? Once done, please open a new issue.

@joacub
Copy link

joacub commented Jun 14, 2023

I did, but seems to be working, the exact replication, I don't know...

https://codesandbox.io/s/nervous-wood-fnhxyz?file=/src/App.tsx

@joacub
Copy link

joacub commented Jun 14, 2023

the thing is probably there is nothing in the store so is always the same, the resulting values.

in my application there is updates and async updates that updates the atom and that's maybe the root of the problem

@joacub
Copy link

joacub commented Jun 14, 2023

it just happen in the mount, if the app was already mount and does that, everything works, maybe the cause is the use of use hook ?? not been supported in react-native

--- edit --- I see you reexported the method, so it should work. I really don't know what happen but it was working before. and only happens in the first mount

@joacub
Copy link

joacub commented Jun 14, 2023

I got the issue, and yes is about the async new features or the better handling... the throw that return this :

const state = useAtomValue(selectAtom(rootAtom, getValues));

is breaking the react flow in react native, I maybe need to wrap this in a suspense, but I wasn't specting this as the useAtomValue to be asynchronous and returning a throw promise, this is what is breaking everything.

why it does not happen if I don't use the selectAtom ?

@dai-shi
Copy link
Member

dai-shi commented Jun 14, 2023

I'm a bit confused, it is a new issue introduced in v2.2.0, or is it an issue in selectAtom that's there before that? Or, is it an issue even without selectAtom? Thanks for your investigation!

@nikolaigeorgie
Copy link

I tried reproducing the issue in a sample repo with next.js and i'm not seeing any unecessary rerenders, heres a little demo
image
image

Heres a sample repo of my demo but the issue seems to be resolved (if there was ever one) i thought also i had the same issue since i'm using heavily jotai with react query but all the rerenders seem to be because of a lack of useMemo and useCallback. Maybe i'm wrong but i think it's clear now. Thanks @dai-shi for all the hard work! 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants