Roman Roman
Roman Roman

Reputation: 917

Why useEffect doesn't call callback function if state doesn't change?

I have the following code(react 16.8.6, react-dom: 16.8.6):

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

function App(props) {
   const [counter, setCounter] = useState(0);

   console.log(1)

   useEffect(() => {
    console.log(2)

     setTimeout(() => {
       console.log(3)

       setCounter(1);
     }, 10000)
   });

   return  counter;
 }

ReactDOM.render(<App />, document.getElementById('root'));

When App component is rendered first time, it prints:

1 2

After 10 seconds it prints:

3, 1, 2

Everything that was before is understandable for me, but now after 10 seconds it prints

3, 1

i.e. function that is passed to useEffect isn't called. I assume it's related to state somehow (as it doesn't change, if it changes useEffect works fine). Could you explain this behaviour?

Upvotes: 3

Views: 3732

Answers (3)

Jonas Wilms
Jonas Wilms

Reputation: 138277

From the React docs:

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.

Thats exactly what happens here: The component rerenders, but as the current state is 1 and you set it to 1 after the second timeout React "bails out" and thus does not retrigger the effect.


Sidenote: The effect should really only depend on counter, not on an update in general.

Upvotes: 2

Domino987
Domino987

Reputation: 8774

Every time your component renders, useEffect will be called because you do not provide an array as a second parameter.

If you read the docs, you will read, that if you do not provide a second parameter, it will be called every time.

If you provide an empty array, it will only be called on the first render.

If you provide a variable into the array, it will only be executed if that variable changes.

3,1 will be called because after the 2 is called, the timeout is set again and will print 3. After updating the state with setCounter(1);, the component rerenders and 1 will be called again.

Hope this helps. Happy coding.

Upvotes: 1

kockburn
kockburn

Reputation: 17616

According to the docs

Does useEffect run after every render? Yes! By default, it runs both after the first render and after every update. (We will later talk about how to customize this.) Instead of thinking in terms of “mounting” and “updating”, you might find it easier to think that effects happen “after render”. React guarantees the DOM has been updated by the time it runs the effects.

An update never occurred after you call setCounter a second time, because 1 === 1 is always the same.

If you actually increment your counter by one every time you'll get your desired effect.

function App(props) {
   const [counter, setCounter] = useState(0);

   console.log(1)

   useEffect(() => {
    console.log(2)

     setTimeout(() => {
       console.log(3)

       setCounter(counter + 1);
     }, 10000)
   });

   return counter;
}

Live example

Upvotes: 3

Related Questions