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

Potential infinite re-rendering when using unstable-identity selector #2013

Closed
1 task done
tgohn opened this issue May 25, 2023 · 2 comments
Closed
1 task done

Potential infinite re-rendering when using unstable-identity selector #2013

tgohn opened this issue May 25, 2023 · 2 comments

Comments

@tgohn
Copy link

tgohn commented May 25, 2023

What version of React, ReactDOM/React Native, Redux, and React Redux are you using?

  • React: v18.2.0
  • ReactDOM: v18.2.0
  • Redux: @reduxjs/toolkit v1.9.3
  • React Redux: v8.0.5

What is the current behavior?

I encountered an infinite React re-rendering loop bug when upgrading an existing code repo from react-redux v7 -> v8

Reproduction sandbox: https://codesandbox.io/s/redux-v8-issue-vr25b1?file=/src/features/counter/Counter.js

Code sample:

  const [boolValue, setBoolValue] = useState(false);

  const _count = useSelector((state) => {
    // NOTE: we return new array here
    return [];
  });

  useEffect(() => {
    // NOTE: we always set to `true` here
    setBoolValue(true);
  }, [_count]); // effect depends on an unstable-idenity value `_count`

The above code sandbox has a few notable points:

  • React version v18
  • the selector callback function used in useSelector(callback) does NOT have a stable-identity (Object.is equals)
  • the selector callback returns an selectedValue object which does NOT have stable-identity
  • there is an useEffect() relying on the above selectedValue in its dependencies list
  • the component's state should not change after 2nd render because setBoolValue(true); always

In the code sandbox, we observe that the component keeps re-rendering itself.
Without specific short-circuit code:

    // Stop infinite loop after certain render count
    if (window.renderCount >= STOP_RENDER) return;

the component will re-render infinitely.

What is the expected behavior?

I am not sure if this is a bug / new expected behaviour in react-redux v8

Granted the existing old code was not ideal:

  • the selector callback function used in useSelector(callback) does NOT have stable-identity (Object.is equals)
  • the selector callback returns an selectedValue object which does NOT have stable-identity

However, when reading the code, we do not expect the component to re-render infinitely:

  • eventually, the component will settle because:
    • there are no new update from the state store
    • the component's state will stop changing because of setBoolValue(true); always
  • the code was working in previous react-redux v7

I am not too familiar with internal of react-redux, could this behaviour is a by product of this useSyncExternalStore behaviour ?

I tested a few things using the above codesandbox, and these help preventing the infinite loop:

  • downgrade React to v17
  • or downgrade react-redux to v7
  • or ensure the selector callback function has stable-identity
  • or ensure the returned selectedValue object has stable-identity

Which browser and OS are affected by this issue?

Chrome Version 114.0.5735.45 (Official Build) beta (x86_64)

Did this work in previous versions of React Redux?

  • Yes
@markerikson
Copy link
Contributor

markerikson commented May 25, 2023

useSelector, useEffect, and setState() have known defined behaviors based on object references. useSelector re-renders the component if you return a new reference. useEffect executes if a value in the deps array is a new reference. setState() forces a re-render if you pass in a new reference as the value.

Note that we do explicitly describe useSelector's behavior in the docs and tell you not to write selectors that always return a new reference unconditionally:

In other words, a selector should only return a new reference if the input data has truly changed.

Now, given the example code you showed, I would not expect that component to keep re-rendering. React should see that you're passing in a value that is the same as the existing state value, and bail out:

So I don't immediately know why this component keeps re-rendering multiple times.

But in general: both React and React-Redux expect that you only create new references when values actually change, and rendering behavior does rely on you following those rules correctly.

@tgohn
Copy link
Author

tgohn commented May 25, 2023

Thank you for your fast reply and confirmation 🙇

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

No branches or pull requests

2 participants