skip to Main Content

I can’t figure out why React is re-rendering my component but not re-running the useEffect hooks.

I was playing around with React hooks using this code https://codesandbox.io/s/solitary-wave-s4wt3r?file=/src/index.js

import React, { useState, useEffect, useRef } from "react";
import ReactDOM from "react-dom";

function Example() {
  const [test, setTest] = useState("init");
  const renderNum = useRef(0);

  console.log("renderNum: ", renderNum.current);
  renderNum.current++;

  useEffect(() => {
    console.log("first useEffect value: ", test);
    setTest("newval");
  });

  useEffect(() => {
    console.log("useEffect value: ", test);
  });
  return <div>{test + renderNum.current}</div>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);

I was surprised by the output:

renderNum:  0
first useEffect value:  init 
useEffect value:  init 
renderNum:  1
first useEffect value:  newval 
useEffect value:  newval 
renderNum:  2

I would have expected either the last render to never happen (since test is being set to the same value it currently is) or both useEffect calls to run.

Any idea why React does the extra render but doesn’t run the effects?

2

Answers


  1. The behavior you are observing is because you didn’t put any dependency in the useEffect, so useEffect will trigger on each rerender on this component, now the component rerender because the state has changed due to setTest("newval")

    But this will retrigger once more useEffect hook (since no dependency) and setTest("newval") in the useEffect hook run again … this should have normally triggered an infinite loop even with the same value in setState, however in this case React is smart enough to optimize rendering by using a process called "reconciliation," where it compares the previous render with the current render and decides whether to apply the changes or not, in other word, React performs an optimization that prevents unnecessary rerender when the state doesn’t actually change, so it skip the rerendering and stop at the third rerender.

    Anyway this is not really a good practice, you can put an empty dependency to make useEffect render ONLY once, or technically you can also put test state in the dependency but this is a bad example, in theory this can also cause infinite loop since this time useEffect will also run if the test state changed, and you put the setter setTest inside the useEffect containing the test state dependency… it’s just in this case you only use setTest with the same value so the component didn’t rerender.

    It’s generally recommended to use useEffect with an empty dependency array [] when you want to run the effect only once on mount, or with specific dependencies that directly relate to the effect’s logic.

      useEffect(() => {
        console.log("first useEffect value: ", test);
        setTest("newval");
      }, []);  // empty array will trigger useEffect only once on mount
    
      useEffect(() => {
        console.log("useEffect value: ", test);
      });
      return <div>{test + renderNum.current}</div>;
    }
    
    Login or Signup to reply.
  2. In React useEffect, useMemo, and useCallback need you to specify dependencies to know when it needs to be rerun.

    useEffect(() => {
      console.log('will render only one time at component creation')
    }, []);
    
    useEffect(() => {
      console.log('will render each time myValue change')
    }, [myValue]);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search