Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

useEffect Hook Example: What causes the re-render?

I am trying to figure out when useEffect causes a re-render. I am very surprised by the result of the following example:

https://codesandbox.io/embed/romantic-sun-j5i4m

function useCounter(arr = [1, 2, 3]) {   const [counter, setCount] = useState(0);   useEffect(() => {     for (const i of arr) {       setCount(i);       console.log(counter);     }   }, [arr]); }  function App() {   useCounter();   console.log("render");   return <div className="App" />; } 

The result of this example is as follows:

enter image description here

I don't know why:

  1. The component renders only three times (I would have guessed the component would rerender for every call to setCount + one initial render - so 4 times)
  2. The counter only ever has two values 0 and 3: I guess, as this article states, every render sees its own state and props so the entire loop will be run with each state as a constant (1, 2, 3) --> But why is the state never 2?
like image 542
Xen_mar Avatar asked Jun 14 '19 13:06

Xen_mar


People also ask

What causes useEffect to Rerender?

Changing state will always cause a re-render. By default, useEffect always runs after render has run. This means if you don't include a dependency array when using useEffect to fetch data, and use useState to display it, you will always trigger another render after useEffect runs.

Do hooks cause re-render?

Every state change in a hook, whether it affects its return value or not, will cause the “host” component to re-render.

Does useEffect Rerender the component?

Inside, useEffect compares the two objects, and since they have a different reference, it once again fetches the users and sets the new user object to the state. The state updates then triggers a re-render in the component.

How do I stop useEffect from Rerendering?

You can add a condition in the call back function that checks if a certain condition is met, e.g. if data is empty. If it is empty, then fetch data, otherwise do nothing. This will prevent the infinite loop from happening. const getData = useEffect(()=>{ const fetchData = () => { UserService.


2 Answers

I'm going to do my best to explain(or walk through) what is happening. I'm also making two assumptions, in point 7 and point 10.

  1. App component mounts.
  2. useEffect is called after the mounting.
  3. useEffect will 'save' the initial state and thus counter will be 0 whenever refered to inside it.
  4. The loop runs 3 times. Each iteration setCount is called to update the count and the console log logs the counter which according to the 'stored' version is 0. So the number 0 is logged 3 times in the console. Because the state has changed (0 -> 1, 1 -> 2, 2 -> 3) React sets like a flag or something to tell itself to remember to re-render.
  5. React has not re-rendered anything during the execution of useEffect and instead waits till the useEffect is done to re-render.
  6. Once the useEffect is done, React remembers that the state of counter has changed during its execution, thus it will re-render the App.
  7. The app re-renders and the useCounter is called again. Note here that no parameters are passed to the useCounter custom hook. Asumption: I did not know this myself either, but I think the default parameter seems to be created again, or atleast in a way that makes React think that it is new. And thus because the arr is seen as new, the useEffect hook will run again. This is the only reason I can explain the useEffect running a second time.
  8. During the second run of useEffect, the counter will have the value of 3. The console log will thus log the number 3 three times as expected.
  9. After the useEffect has run a second time React has found that the counter changed during execution (3 -> 1, 1 -> 2, 2 -> 3) and thus the App will re-render causing the third 'render' log.
  10. Asumption: because the internal state of the useCounter hook did not change between this render and the previous from the point of view of the App, it does not execute code inside it and thus the useEffect is not called a third time. So the first render of the app it will always run the hook code. The second one the App saw that the internal state of the hook changed its counter from 0 to 3 and thus decides to re-run it, and the third time the App sees the internal state was 3 and is still 3 so it decides not to re-run it. That's the best reason I can come up with for the hook to not run again. You can put a log inside the hook itself to see that it does not infact run a third time.

This is what I see happening, I hope this made it a little bit clearer.

like image 87
ApplePearPerson Avatar answered Sep 30 '22 01:09

ApplePearPerson


I found an explanation for the third render in the react docs. I think this clarifies why react does the third render without applying the effect:

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

It seems that useState and useReducer share this bail out logic.

like image 43
Xen_mar Avatar answered Sep 30 '22 02:09

Xen_mar