Home > Software design >  useState leads to infinite rendering - understanding the solution
useState leads to infinite rendering - understanding the solution

Time:10-16

I have a weird issue which occurs in very few lines of code, and I do not understand what's happening in the back of the scenes.

I have the following 4 lines of code:

function FarmerComponent(props) {
  let authCtx = useContext(AuthContext)
  let usersAndItemsCtx = useContext(usersAndProductsContext)
  
  let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);

  let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(false)
  console.log(current_logged_user.likedProfilesId)
    let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
    isCurrentFarmerLikedFlagToggle(flag);
    console.log(flag)
  

this produces for me the following error:

react-dom.development.js:16317 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. at renderWithHooks (react-dom.development.js:16317:1) at updateFunctionComponent (react-dom.development.js:19588:1) at beginWork (react-dom.development.js:21601:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1) at invokeGuardedCallback (react-dom.development.js:4277:1) at beginWork$1 (react-dom.development.js:27451:1) at performUnitOfWork (react-dom.development.js:26557:1) at workLoopSync (react-dom.development.js:26466:1) at renderRootSync (react-dom.development.js:26434:1)

while also logs many times 'false'.

Now, if I change these lines as the following:

function FarmerComponent(props) {
  let authCtx = useContext(AuthContext)
  let usersAndItemsCtx = useContext(usersAndProductsContext)
  
  let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);

  console.log(current_logged_user.likedProfilesId)
  let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
  console.log(flag)
  let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(flag)

everything works like a charm.

Why would that even happen? de facto, nothing I'm aware of has changed.. so why such a drastic difference in the result ?

Regards

CodePudding user response:

de facto, nothing I'm aware of has changed

The thing that's changed is that you're no longer calling your state setter (isCurrentFarmerLikedFlagToggle) at the top level of your function component. You can't call state setters at the top level of a function component (even if you're setting the same value, but in your case, you may not be, it could have changed between renders), because the function component is called during the render phase, which must be pure (other than scheduling side effects to happen later).

But there's a more fundamental issue: there's no need for that flag to be in state at all. Copying stateful information the component receives (as props, as context, etc.) into component state is, in general, an antipattern (more here). In your example, you don't need isCurrentFarmerLikedFlag to be a state member; it's value is derived purely from information your component is already being provided: authCtx.currentLoggedUserId and usersAndItemsCtx.likedProfilesId:

let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);

let current_logged_user = usersAndItemsCtx.usersVal.find(
    (user) => user.id === authCtx.currentLoggedUserId
);

let isCurrentFarmerLikedFlag = current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false;
//  ^^^^^^^^^^^^^^^^^^^^^^^^

If you thought the work involved in determining that value was too much to do on every render, you can memoize the value via useMemo (or any of various other memoization helpers):

let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);

let current_logged_user = useMemo(
    () => usersAndItemsCtx.usersVal.find((user) => user.id === authCtx.currentLoggedUserId),
    [authCtx.currentLoggedUserId, usersAndItemsCtx.usersVal]
);
let isCurrentFarmerLikedFlag = useMemo(
    () => (current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false),
    [current_logged_user.likedProfilesId]
);

(More notes on that code below.)

The callback calculates the value, and the dependency array tells useMemo when it should call the callback again because something changed. Anything you use directly in the calculation should be listed in the dependency array. (But note that there's no need to list the container something comes from; we have authCtx.currentLoggeduserId in the first dependency array above, for instance, not both authCtx and authCtx.currentLoggeduserId. We don't care if authCtx changed but authCtx.currentLoggeduserId didn't, because we don't use authCtx except for its authCtx.currentLoggeduserId value.)


A few side notes:

  • While you can (of course) use any naming convention you like in your own code, I urge you to follow standard practice when writing code you'll share with others. The standard practice for naming state setters is that they have the name of the state variable (with its first letter capitalized) prefixed with the word set. So if the flag is isCurrentFarmerLikedFlag, the setter should be setIsCurrentFarmerLikedFlag. (Separately, isCurrentFarmerLikedFlagToggle is a particularly problematic name because it doesn't toggle the value, it accepts a new value to set [which may or may not be toggled.])
  • This code:
    current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false
    
    can more succinctly and idiomatically be written (but keep reading):
    current_logged_user.likedProfilesId.some((id) => id === props.id)
    
    Any time you want to just know if an array has an element that matches the predicate function you pass in, you can use some rather than find. But, since you don't actually need a predicate function at all (you're just checking for the direct existence of a value), we can go even further and use includes:
    current_logged_user.likedProfilesId.includes(props.id)
    
  1. All of the let declarations in that code could be const instead, and I'd urge you to use const because directly modifying their values (isCurrentFarmerLikedFlag = newValue) won't work and is misleading.

Taking all of that into account:

const authCtx = useContext(AuthContext);
const usersAndItemsCtx = useContext(usersAndProductsContext);

const current_logged_user = useMemo(
    () => usersAndItemsCtx.usersVal.find((user) => user.id === authCtx.currentLoggedUserId),
    [authCtx.currentLoggedUserId, usersAndItemsCtx.usersVal]
);
const isCurrentFarmerLikedFlag = useMemo(
    () => current_logged_user.likedProfilesId.includes(props.id),
    [current_logged_user.likedProfilesId]
);

CodePudding user response:

I think your problem was the calling function isCurrentFarmerLikedFlagToggle without any condition so, every time when your component renders, you also update the state which results in the rendering of your components again and again.

function FarmerComponent(props) {
  let authCtx = useContext(AuthContext)
  let usersAndItemsCtx = useContext(usersAndProductsContext)
  
  let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);

  let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(false)
  console.log(current_logged_user.likedProfilesId)
    let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
    isCurrentFarmerLikedFlagToggle(flag); // calling the function right after
    console.log(flag)

and when you did not call it like that, it works fine as your below code.

function FarmerComponent(props) {
  let authCtx = useContext(AuthContext)
  let usersAndItemsCtx = useContext(usersAndProductsContext)
  
  let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);

  console.log(current_logged_user.likedProfilesId)
  let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
  console.log(flag)
  let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(flag) // using the flag for initial value of state in useState
  • Related