preactjs / preact

⚛️ Fast 3kB React alternative with the same modern API. Components & Virtual DOM.
https://preactjs.com
MIT License
36.35k stars 1.93k forks source link

[preact/hooks] useMemo keeps recalculating even when the dependency is not changed #4422

Closed navahg closed 1 week ago

navahg commented 1 week ago

Describe the bug When using the useMemo hook to calculate a value and then using this value to conditionally update the state during render, the app indefinitely re-renders. Something like shown below

const [value, setValue] = useState(false);

const data = useMemo(() => {
  return { data: 'some data' };
}, [value]);

const [prevData, setPreviousData] = useState(data);

if (prevData !== data) {
  // This causes the infinite loop
  setPreviousData(data);
}

Preact version - 10.21.0

To Reproduce

Sandbox - https://codesandbox.io/p/sandbox/usememo-behavior-preact-w3rlrv

Steps to reproduce the behavior:

  1. Go to the sandbox and open the console
  2. Click on the 'Set to true' button
  3. See error (and the console log of the memoized value changes)

After some debugging, it seems like the internalHookState of the useMemo hook is not updated with the new dependency args if a state update happens during the render cycle. And the hook always ends up comparing the dependency array with the very first dependency array. This is resulting in a recalculation of the hook every render and the component re-renders indefinitely.

Expected behavior The useMemo hook should remember the last provided dependency array and should only recalculate the value when there is an actual change in the dependency array.

React seems to be handling this case as expected - https://codesandbox.io/p/sandbox/usememo-behavior-react-l3cxhs

webvs2 commented 1 week ago

I can try to figure out why😁

JoviDeCroock commented 1 week ago

Hey,

The context behind the _args not being set immediately can be found here. When comparing this to React they do succeed in making this work.

Maybe diffed is too late in the cycle to apply the _pendingArgs or maybe we need to check whether _pendingArgs changed when they are present. The CSB does imply that useMemo does not rerun in React.

I personally don't really have time for this at the moment but it probably warrants a deeper investigation in these bail cases for _pendingArgs/... We have to ensure that in effects we don't queue them up multiple times, that we can see that a state is equal between start and end of rendering to bail and that the memo/callback stuff doesn't risk causing an infinite loop. They don't seem to have any code for this so I assume that it's more related to disposing the state.

EDIT: comparing this closely, I think even a few of our tests are asserting incorrect behavior https://stackblitz.com/edit/vitejs-vite-tylqxa?file=src%2FApp.tsx,src%2Fmain.tsx,src%2FMemo.tsx&terminal=dev - this is making me assume that we can just remove all the _pending* stuff for useMemo