Search code examples
reactjsreact-hooksrenderinguse-effectuse-state

Why getting too many re-renders?


I have a Calculator component for calculating the exponent of a given base. My code is as follows:

//Exponential Calculator (Power/Squre-Root/Cube-Root)
  const Exponents=()=>{
    const [result,setResult]=useState(null);
    const [choice,setChoice]=useState("Power");
    const [x,setX]=useState(null);
    const [n,setN]=useState(null);
    useEffect(()=>{

    },[choice,x,n,result]);
    const calcResult=()=>{
      let res=1;
      if(choice=="Power")
      {
        for(var i=1;i<=n;i++)
        res*=x;
      }
      else if(choice=="SquareRoot")
      res=Math.sqrt(x);
      else
      res=Math.cbrt(x);

      setResult(res);
    }
    const handleChange=(e)=>{
      reset();
      setChoice(e.target.value);
    }
    function reset(){
      setResult(null);
      setX(null);
      setN(null);
    }
    const choiceData=()=>{
      if(choice==="Power"){
        return {
          name:"Power",
          quantities:["Base","Exponent"],
          disabled:false
        }
      }
      else if(choice==="SquareRoot")
      {
        setN(0.50);
        return{
          name:"Square-Root",
          quantities:["Base","Exponent"],
          disabled:true
        }
      }
      else if(choice==="CubeRoot")
      { 
        setN(0.34);
        return{
          name:"Cube-Root",
          quantities:["Base","Exponent"],
          disabled:true
        }
      }
    }
    return(
      <>
      <Form>
      <Form.Group className="mb-4" controlId="choice">
            <Form.Label>Select the type of calculation</Form.Label>
            <Form.Control
              as="select"
              className="select-custom-res"
              onChange={(e) => handleChange(e)}
            > 
              <option value="Power">Power</option>
              <option value="SquareRoot">Square Root</option>
              <option value="CubeRoot">Cube Root</option>
            </Form.Control>
          </Form.Group>
          <Form.Group className="mb-4" controlId="text">
            <Form.Text className="text">
              <strong>
                To find the {choiceData().name}, Enter the following values
              </strong>
              <br />
            </Form.Text>
          </Form.Group>
          <Form.Group className="mb-4">
            <Form.Label>{choiceData().quantities[0]}</Form.Label>
            <Form.Control
              onChange={(e) => setX(e.target.value)}
              type="number"
              placeholder={"Enter the Base"}
              value={x === null ? "" : x}
            />
          </Form.Group>
          <Form.Group className="mb-4">
            <Form.Label>{choiceData().quantities[1]}</Form.Label>
            <Form.Control
              onChange={(e) => setN(e.target.value)}
              type="number"
              placeholder={"Enter the Exponent"}
              value={n === null ? "" : n}
              disabled={choiceData().disabled}
            />
          </Form.Group>
          <Form.Group className="mb-4">
            <Form.Control
              readOnly
              type="number"
              placeholder={result === null ? "Result" : result + " "}
            />
          </Form.Group>
      </Form>
        <div className="button-custom-grp">
          <Button variant="primary" onClick={calcResult}>
            Calculate
          </Button>
          &nbsp;&nbsp;&nbsp;
          <Button variant="dark" onClick={() => reset()} type="reset">
            Reset
          </Button>
        </div>
      </>
    )
  }

On changing the choice from Power to SquareRoot I get an error:Too many rerenders. Interestingly, when I remove the setState line in the choiceData function,the error vanishes. Although I have used useEffect to prevent re-renders, it's not working.


Solution

  • Issue

    You call choiceData in the render return, which should be free of side-effects, like enqueueing state updates. When you call setN in the choiceData function it triggers a rerender, which calls choiceData again, which triggers a rerender... repeat ad nauseam.

    Solution

    I suggest converting choiceData into a chunk of state, and use an useEffect hook to update it and the n state depending on the choice state. In the render return, instead of calling a function to get a value, i.e. choiceData().quantities[0], you instead just access a property of the new choiceData state, i.e. choiceData.quantities[0].

    const Exponents = () => {
      const [result, setResult] = useState(null);
      const [choice, setChoice] = useState("Power");
      const [choiceData, setChoiceData] = useState({});
      const [x, setX] = useState(null);
      const [n, setN] = useState(null);
    
      useEffect(() => {
        if (choice === "Power") {
          setChoiceData({
            name: "Power",
            quantities: ["Base", "Exponent"],
            disabled: false
          });
        } else if (choice === "SquareRoot") {
          setN(0.5);
          setChoiceData({
            name: "Square-Root",
            quantities: ["Base", "Exponent"],
            disabled: true
          });
        } else if (choice === "CubeRoot") {
          setN(0.34);
          setChoiceData({
            name: "Cube-Root",
            quantities: ["Base", "Exponent"],
            disabled: true
          });
        }
      }, [choice]);
    
      useEffect(() => {
        // is this effect used for anything?
      }, [choice, x, n, result]);
    
      const calcResult = () => {
        let res = 1;
        if (choice == "Power") {
          for (var i = 1; i <= n; i++) res *= x;
        } else if (choice == "SquareRoot") res = Math.sqrt(x);
        else res = Math.cbrt(x);
    
        setResult(res);
      };
    
      const handleChange = (e) => {
        reset();
        setChoice(e.target.value);
      };
    
      function reset() {
        setResult(null);
        setX(null);
        setN(null);
      }
    
      return (
        <>
          <Form>
            <Form.Group className="mb-4" controlId="choice">
              <Form.Label>Select the type of calculation</Form.Label>
              <Form.Control
                as="select"
                className="select-custom-res"
                onChange={(e) => handleChange(e)}
              >
                <option value="Power">Power</option>
                <option value="SquareRoot">Square Root</option>
                <option value="CubeRoot">Cube Root</option>
              </Form.Control>
            </Form.Group>
            <Form.Group className="mb-4" controlId="text">
              <Form.Text className="text">
                <strong>
                  To find the {choiceData.name}, Enter the following values
                </strong>
                <br />
              </Form.Text>
            </Form.Group>
            <Form.Group className="mb-4">
              <Form.Label>{choiceData.quantities[0]}</Form.Label>
              <Form.Control
                onChange={(e) => setX(e.target.value)}
                type="number"
                placeholder={"Enter the Base"}
                value={x === null ? "" : x}
              />
            </Form.Group>
            <Form.Group className="mb-4">
              <Form.Label>{choiceData.quantities[1]}</Form.Label>
              <Form.Control
                onChange={(e) => setN(e.target.value)}
                type="number"
                placeholder={"Enter the Exponent"}
                value={n === null ? "" : n}
                disabled={choiceData.disabled}
              />
            </Form.Group>
            <Form.Group className="mb-4">
              <Form.Control
                readOnly
                type="number"
                placeholder={result === null ? "Result" : result + " "}
              />
            </Form.Group>
          </Form>
          <div className="button-custom-grp">
            <Button variant="primary" onClick={calcResult}>
              Calculate
            </Button>
            &nbsp;&nbsp;&nbsp;
            <Button variant="dark" onClick={() => reset()} type="reset">
              Reset
            </Button>
          </div>
        </>
      );
    };