40

I have set up an effect inside my component, which changes the view if another state attribute changes. But for some reason, when the component mounts, the effect is run, even though the value of detailIndex has not changed.

const EventsSearchList = () => {
    const [view, setView] = useState('table');
    const [detailIndex, setDetailIndex] = useState(null);

    useEffect(() => {
        console.log('onMount', detailIndex);
        // On mount shows "null"
    }, []);


    useEffect(
        a => {
            console.log('Running effect', detailIndex);
            // On mount shows "null"!! Should not have run...
            setView('detail');
        },
        [detailIndex]
    );

    return <div>123</div>;

};

Why is this happening?

UPDATE: In case it is not clear, what I am trying is to run the effect when the component updates because detailIndex changes. NOT when it mounts.

Enrique Moreno Tent
  • 24,127
  • 34
  • 104
  • 189
  • 1
    `useEffects` always fires on mount AFAIK. – dan-klasson Feb 28 '19 at 10:52
  • 2
    It was my understanding that this only happens if the second parameters is `[]` – Enrique Moreno Tent Feb 28 '19 at 10:52
  • That's used for when re-rendering the component I think. What are you trying to accomplish? – dan-klasson Feb 28 '19 at 10:53
  • 1
    @EnriqueMorenoTent no, the second parameter defines when to rerun this effect, and [detailIndex] means it will rerun every time detailIndex changes ([] means there is no rerun). – r g Feb 28 '19 at 10:53
  • 1
    Just a stupid guess but have you tried `useState(undefined)` instead on `useState(null)`. My dumb first thought is that, your state was previously undefined and you just set it to null, so useEffect is triggered. But it is more likely that useEffect is always executed on mount even though you passed a dependecy value. – nubinub Feb 28 '19 at 10:56
  • @nubinub it wont work, because this state variable was never initialized yet and its different from undefined; ``console.log(neverInitializedVariable === undefined) // Uncaught ReferenceError: neverInitializedVariable is not defined`` – r g Feb 28 '19 at 11:03
  • @dan-klasson I am trying to run the effect when the component updates, BUT not when it mounts – Enrique Moreno Tent Feb 28 '19 at 11:17
  • @fard In my opinion this is more dependant on how hooks were implemented than javascript scpecification. – nubinub Feb 28 '19 at 11:36
  • @nubinub but if its not in javascript then you shouldn't relay on such hidden mechanics and i don't think it is implemented in this way – r g Feb 28 '19 at 12:11
  • Yeah I get that :) But I mean what's your end-goal? Maybe there's another way – dan-klasson Feb 28 '19 at 12:11

6 Answers6

25

useEffect from React Hooks is by default executed on every render, but you can use second parameter in function to define when the effect will be executed again. That means that function is always executed on mount. In your situation your second useEffect will be run on start and when detailIndex changes.

More info: https://reactjs.org/docs/hooks-effect.html

Source:

Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render. [...] You can tell React to skip applying an effect if certain values haven’t changed between re-renders. To do so, pass an array as an optional second argument to useEffect: [...]

r g
  • 3,586
  • 1
  • 11
  • 27
  • 4
    It's also worth noting that the function in `useEffect` is run **after** the render, rather than before. Very easy to forget as these definitions are usually at the top of the function component definitions. – Shiraz Feb 05 '21 at 15:36
  • can anyone please explain to me what this statement means from the docs? `Experienced JavaScript developers might notice that the function passed to useEffect is going to be different on every render.` We pass an arrow function to useEffect and it is called everytime after render, from the closure the state and prop values might be different to what it has, but how come the function itself is different? – Chaitanya Tetali May 11 '22 at 19:47
  • @ChaitanyaTetali Declaration of function happens at every render, but `useEffect` calls the function according to dependency array. The state values might be different only when component didn't rerender yet. It might happen eg. when there are multiple state changes in single mouse click handler. – r g May 25 '22 at 09:28
24

In my case, the component kept updating even though I used the second argument in useEffect() and I was printing the argument to make sure it did not change and it did not change. The problem was that I was rendering the component with map() and there were cases where the key changed, and if the key changes, for react, it is a completely different object.

Andre
  • 15
  • 4
Baroudi Safwen
  • 803
  • 6
  • 17
7

One way to make sure code in useEffect only runs when a dependency has changed, and not on initial mount (first render), is to assign a variable that is initially false and only becomes true after the first render, and this variable naturally should be a hook, so it will be memo-ed across the component's lifespan.

Comparison of 3 different useEffect:

const {useEffect, useRef, useState} = React

const App = () => {
  const mountedRef = useRef()                 // ← the "flag"
  const [value, setValue] = useState(false)   // simulate a state change 
  

  // with the trick
  useEffect(() => {
      if (mountedRef.current){                // ← the trick
          console.log("trick: changed")
      }
  }, [value])


  // without the trick
  useEffect(() => {
     console.log("regular: changed")
  }, [value])


  // fires once & sets "mountedRef" ref to "true"
  useEffect(() => {
    console.log("rendered")
    mountedRef.current = true
    // update the state after some time
    setTimeout(setValue, 1000, true)
  }, [])
    
  return null
}

ReactDOM.render(<App/>, document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
vsync
  • 118,978
  • 58
  • 307
  • 400
5

You can add a second guard, check if detailIndex has changed from the initial value says -1

 useEffect(
    a => {
     if(detailIndex != -1)
      {  console.log('Running effect', detailIndex);
        // On mount shows "null"!! Should not have run...
        setView('detail');
    }},
    [detailIndex]);
Dan Hunex
  • 5,172
  • 2
  • 27
  • 38
  • 3
    Is a guard the only way to do this? It seems so hacky for a framework like React – FabricioG Aug 28 '20 at 00:01
  • @FabricioG We can use useRef() instead, checkout this excellent explanation in stackoverflow: https://stackoverflow.com/questions/55075604/react-hooks-useeffect-only-on-update – Hemanth Jul 29 '21 at 20:18
3

useEffect is always ran after the initial render.

From 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.

Regarding your code, the following will only run once since no dependencies are specified (useEffect takes an optional array of dependencies as a second argument):

useEffect(() => {
  console.log('onMount', detailIndex);
  // On mount shows "null" -> since default value for detailIndex is null as seen above
  // const [detailIndex, setDetailIndex] = useState(null);
}, []);

And this effect will trigger both after the initial render and when the detailIndex changes (try calling setDetailIndex within the previous useEffect):

useEffect(() =>
  // ... effect code
}, [detailIndex]);

useEffect hook API reference

Pa Ye
  • 1,779
  • 12
  • 22
  • Hi Pavel, I think your answer contradicts itself. You first say that useEffect is always triggered by the initial render (it is), then you say in your final example that it will run only when ```detailIndex``` changes. This is not true, as it will also run after the initial render. – liquidki Nov 17 '21 at 13:19
  • 1
    Hey @liquidki, I didn't mean it this way - I guess my wording could've been more clear. What you are saying is correct - the effect in the final example will run both after the initial render and every time the `detailIndex` changes. Thanks for pointing this out - I've corrected my answer. – Pa Ye Nov 19 '21 at 09:15
3

If and only if the useEffect's second parameter state changes by reference (not value), the effect fires.

import React, { useState, useEffect } from 'react';
    function App() {
        const [x, setX] = useState({ a: 1 });
        useEffect(() => {
            console.log('x');
        }, [x]);
        setInterval(() => setX({ a: 1 }), 1000);
        return <div></div>
    }
}
export default App;

In this code snippet, the log is printed in every second, because, every time setX({a: 1}) is called, x state is updated with a new reference to a new object.

If setX({a: 1}) was replaced by setX(1), effect would not fire periodically.

Emre Tapcı
  • 1,743
  • 17
  • 16