Home > Software design >  How to use useEffect to change useState whenever a var is changed without triggering an infinite loo
How to use useEffect to change useState whenever a var is changed without triggering an infinite loo

Time:09-23

in my react component, I am getting the following issue in my console

index.js:1 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. at Id (http://localhost:3000/main.82c936520e7a7a921612.hot-update.js:32:27) at div at div at div at App (http://localhost:3000/static/js/main.chunk.js:249:62)

The number of time that error shows keeps increasing each new second, the same error is created again. I have no idea why because I thought my logic makes sense, how is it creating an infinite loop?

import React, { useEffect } from "react";
import ReactTooltip from "react-tooltip";

import { FaQuestionCircle, FaRedoAlt } from 'react-icons/fa';

interface Props {
    setId: React.Dispatch<React.SetStateAction<{data: number, componentRef: React.RefObject<HTMLDivElement>}>>;
    id : {data: number, componentRef:React.RefObject<HTMLDivElement>};
}

const Id : React.FC<Props> = ( props : Props ) => {
    
    var idData = {...props.id};

    useEffect(()=>{
        props.setId(idData);
    }, [idData]);

    const generateNewId = () => {
        idData.data = Math.floor( Math.random()*1000 )   1; 
    }

    useEffect(()=>{
        generateNewId();
    },[])

    const modifyId = (sign : string) => {
        sign === " " ? idData.data = idData.data   1 : idData.data = idData.data - 1 ;
    }

    return(
        <div>
            <ReactTooltip id="counter-info">
                <p>A random ID is generated each time you click refresh</p>
                <p className="text-bold">What is ID?</p>
                <p className="text-bold">Your ID is added to the back of your username, to always ensure that you have a unique username</p>
            </ReactTooltip> 
            <h2>Your ID: {idData.data}
                <span data-tip data-for="counter-info">
                    <FaQuestionCircle/>
                </span>
            </h2>
            <button className="btn rounded-circle btn-dark" onClick={()=>modifyId("-")}>-</button>
            <button className="btn rounded-circle btn-dark mr-3" onClick={()=>modifyId(" ")}> </button>
            <button className="btn" onClick={()=>generateNewId()}><FaRedoAlt/></button>
        </div>
    )
    
}

export default Id;

In App.tsx

const idRef = React.useRef<HTMLDivElement>(null);
const [ id, setId ] = useState<{data: number, componentRef: React.RefObject<HTMLDivElement>}>(
    {
      data: NaN,
      componentRef: idRef
    }
  )
<div ref={idRef} className="col-3">
          <Id id={id} setId={setId}/>
        </div>

CodePudding user response:

Issues

  1. You are setting the id passed to your props, with the setter function passed to your props: (This way every time this component renders, it sets the id, and since id is a prop, a re-render is triggered. Hence, an infinite loop of re-renders)
    var idData = {...props.id};

    useEffect(()=>{
        props.setId(idData);
    }, [idData]);

This is not necessary, since this is a controlled component, and you already passed the id to your child.

  1. You are modifying the props directly in your modifyId function. Instead, assign it to a state variable and make changes to that state variable

Understanding dependency array

In useEffect the dependency array makes sure that, that particular useEffect triggers only depending on your dependency array

useEffect(() => console.log("run once"), [])
useEffect(() => console.log("run only id changes"), [id])

Solving your problem

Your problem is a little bit confusing to understand. Based on my understanding, you want to setId when you modifyId:

const Id : React.FC<Props> = ( props : Props ) => {
    
    const {id: idData} = props
    const [childId, setChildId] = useState({})

    useEffect(() => {
       if(idData) setChildId(idData)
    }, [idData])

    const generateNewId = () => {
        setChildId(Math.floor( Math.random()*1000 )   1)
    }

    const modifyId = (sign : string) => {
        setChildId(id => sign === ' ' ? {...id, data: id.data   1} : {...id, data: id.data-1})
    }

    return(
        <div>
            <ReactTooltip id="counter-info">
                <p>A random ID is generated each time you click refresh</p>
                <p className="text-bold">What is ID?</p>
                <p className="text-bold">Your ID is added to the back of your username, to always ensure that you have a unique username</p>
            </ReactTooltip> 
            <h2>Your ID: {childId.data}
                <span data-tip data-for="counter-info">
                    <FaQuestionCircle/>
                </span>
            </h2>
            <button className="btn rounded-circle btn-dark" onClick={()=>modifyId("-")}>-</button>
            <button className="btn rounded-circle btn-dark mr-3" onClick={()=>modifyId(" ")}> </button>
            <button className="btn" onClick={()=>generateNewId()}><FaRedoAlt/></button>
        </div>
    )
    
}

export default Id;

PS: If you don't want to set a state variable, remove the useState call and replace setChildId with prop.setId

CodePudding user response:

i see several mistakes in your code; passing an object as a dependency to useEffect is not a good idea, objects in js are a reference type, when your component runs again (re-render), each round the idData object will be recreated...

therefore, when the component runs again, the current object is unequal to the previous object that you passed to the useEffect dependency array and useEffect runs again... frequently, this process will be repeated constantly!

e.g:

{} === {} //flase!

in your case, you have to use setId when you wanna set a new id... or if you have to initialize it, you can set an empty array as the useEffect dependency.

  • Related