110

If we want to restrict useEffect to run only when the component mounts, we can add second parameter of useEffect with [].

useEffect(() => {
  // ...
}, []);

But how can we make useEffect to run only when the moment when the component is updated except initial mount?

Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
koo
  • 4,013
  • 6
  • 15
  • 31

10 Answers10

228

If you want the useEffect to run only on updates except initial mount, you can make use of useRef to keep track of initialMount with useEffect without the second parameter.

const isInitialMount = useRef(true);

useEffect(() => {
  if (isInitialMount.current) {
     isInitialMount.current = false;
  } else {
      // Your useEffect code here to be run on update
  }
});
Joel 8 bitar
  • 123
  • 1
  • 3
  • 16
Shubham Khatri
  • 270,417
  • 55
  • 406
  • 400
  • 1
    I thought that useRef can only be used to DOM manipulation. thanks! – koo Mar 09 '19 at 15:02
  • 10
    In fact, this question is listed in [**React FAQ**](https://reactjs.org/docs/hooks-faq.html) and [**here**](https://reactjs.org/docs/hooks-faq.html#can-i-run-an-effect-only-on-updates) here it explicitly says that this is the right way. – Daniel Nov 10 '19 at 15:26
  • 2
    This is it. I suggest understanding `useRef` first and then use [`useUpdateEffect`](https://github.com/streamich/react-use/blob/master/docs/useUpdateEffect.md) from `react-use`. – adi518 Feb 27 '20 at 14:29
  • 2
    I think strict mode breaks this because of the double render - https://codesandbox.io/s/sharp-feynman-9qs09s – TKharaishvili Nov 24 '22 at 20:01
47

I really like Shubham's response, so I made it a custom Hook

/**
 * A custom useEffect hook that only triggers on updates, not on initial mount
 * @param {Function} effect
 * @param {Array<any>} dependencies
 */
export default function useUpdateEffect(effect, dependencies = []) {
  const isInitialMount = useRef(true);

  useEffect(() => {
    if (isInitialMount.current) {
      isInitialMount.current = false;
    } else {
      return effect();
    }
  }, dependencies);
}
Mario Campa
  • 4,092
  • 1
  • 26
  • 25
  • Why are you disabling the eslint rule? – zeckdude Nov 27 '19 at 21:23
  • I couldn't find a way to make it compliant with the rule, so I disabled it. I will remove it from the answer since is not relevant. – Mario Campa Dec 03 '19 at 19:51
  • For typescript I had to change to ` useUpdateEffect(effect: Function, dependencies: any[] = [])` – Rotem May 28 '20 at 09:00
  • 6
    Shouldn't we `return effect();` instead of just calling `effect();` so that we honor the clean up function? – Code Commander Dec 23 '20 at 19:24
  • 1
    Note that when the development build of react is being used, react strictmode will call the `useEffect` twice on the initial mount, thus making the useUpdateEffect run when the component mounts. – Caleb Liu Oct 13 '22 at 12:54
9

Both Shubham and Mario suggest the right approach, however the code is still incomplete and does not consider following cases.

  1. If the component unmounts, it should reset it's flag
  2. The passing effect function may have a cleanup function returned from it, that would never get called

Sharing below a more complete code which covers above two missing cases:

import React from 'react';

const useIsMounted = function useIsMounted() {
  const isMounted = React.useRef(false);

  React.useEffect(function setIsMounted() {
    isMounted.current = true;

    return function cleanupSetIsMounted() {
      isMounted.current = false;
    };
  }, []);

  return isMounted;
};

const useUpdateEffect = function useUpdateEffect(effect, dependencies) {
  const isMounted = useIsMounted();
  const isInitialMount = React.useRef(true);

  React.useEffect(() => {
    let effectCleanupFunc = function noop() {};

    if (isInitialMount.current) {
      isInitialMount.current = false;
    } else {
      effectCleanupFunc = effect() || effectCleanupFunc;
    }
    return () => {
      effectCleanupFunc();
      if (!isMounted.current) {
        isInitialMount.current = true;
      }
    };
  }, dependencies); // eslint-disable-line react-hooks/exhaustive-deps
};
Sankalp Lakhina
  • 121
  • 2
  • 6
6

Shorter One

const [mounted, setMounted] = useRef(false)

useEffect(() => {
  if(!mounted) return setMounted(true)
  ...
})

React Hook Solution

Hook

export const useMounted = () => {
  const mounted = useRef(false)

  useEffect(() => {
    mounted.current = true
    return () => {
      mounted.current = false
    }
  }, [])

  return () => mounted.current
}

Usage

const Component = () => {
  const mounted = useMounted()

  useEffect(() => {
    if(!mounted()) return
    ...
  })
}

Jawad Ahbab
  • 1,462
  • 1
  • 19
  • 31
5

You can get around it by setting the state to a non-boolean initial value (like a null value) :

  const [isCartOpen,setCartOpen] = useState(null);
  const [checkout,setCheckout] = useState({});

  useEffect(() => {

    // check to see if its the initial state
    if( isCartOpen === null ){

      // first load, set cart to real initial state, after load
      setCartOpen( false );
    }else if(isCartOpen === false){

      // normal on update callback logic
      setCartOpen( true );
    }
  }, [checkout]);
awongh
  • 1,308
  • 12
  • 21
4

Took help from Subham's answer This code will only run for particular item update not on every update and not on component initial mounting.

const isInitialMount = useRef(true);    //useEffect to run only on updates except initial mount


//useEffect to run only on updates except initial mount
  useEffect(() => {
    if (isInitialMount.current) {
        isInitialMount.current = false;
     } else {              
         if(fromScreen!='ht1' && appStatus && timeStamp){
            // let timeSpentBG = moment().diff(timeStamp, "seconds");
            // let newHeatingTimer = ((bottomTab1Timer > timeSpentBG) ? (bottomTab1Timer - timeSpentBG) : 0);
            // dispatch({
            //     type: types.FT_BOTTOM_TAB_1,
            //     payload: newHeatingTimer,
            // })
            // console.log('Appstaatus', appStatus, timeSpentBG, newHeatingTimer)
         }
     }
  }, [appStatus])
Ajmal Hasan
  • 885
  • 11
  • 9
2

If you tried Shubham's answer, and the useeffect is still being called on the initial mount, you can easily fix this by disabling React strictmode. But if you don't want to disable strictmode, use this.

// The init variable is necessary if your state is an object/array, because the == operator compares the references, not the actual values.
const init = []; 
const [state, setState] = useState(init);
const dummyState = useRef(init);

useEffect(() => {
  // Compare the old state with the new state
  if (dummyState.current == state) {
    // This means that the component is mounting
  } else {
    // This means that the component updated.
    dummyState.current = state;
  }
}, [state]);

Works in development mode...

function App() {
  const init = []; 
  const [state, setState] = React.useState(init);
  const dummyState = React.useRef(init);

  React.useEffect(() => {
    if (dummyState.current == state) {
      console.log('mount');
    } else {
      console.log('update');
      dummyState.current = state;
    }
  }, [state]);
  
  return (
    <button onClick={() => setState([...state, Math.random()])}>Update state </button>
  );
}

ReactDOM.createRoot(document.getElementById("app")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>

<div id="app"></div>

And in production.

function App() {
  const init = []; 
  const [state, setState] = React.useState(init);
  const dummyState = React.useRef(init);

  React.useEffect(() => {
    if (dummyState.current == state) {
      console.log('mount');
    } else {
      console.log('update');
      dummyState.current = state;
    }
  }, [state]);
  return (
    <button onClick={() => setState([...state, Math.random()])}>Update state </button>
    );
}

ReactDOM.createRoot(document.getElementById("app")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<div id="app"></div>
Caleb Liu
  • 627
  • 5
  • 18
1

To make a custom hook compliant with the rules of hooks you don't need to actually pass dependencies, just wrap your effect function with useCallback

function useEffectOnUpdate(callback) {
  const mounted = useRef();

  useEffect(() => {
    if (!mounted.current) {
      mounted.current = true;
    } else {
      callback();
    }
  }, [callback]);
};

function SomeComponent({ someProp }) {
  useEffectOnUpdate(useCallback(() => {
    console.log(someProp);
  }, [someProp]));

  return <div>sample text</div>;
}
0

When using React strict mode, the solution has to change because React will do the initial render twice and throw an exception if the order of your use of useRef and useEffect is different. I found that the following works as a modification of @shubham-khatri's answer:

const isInitialMount = useRef(true);

useEffect(() => {
  if (isInitialMount.current === true) {
      // Your useEffect code here to be run on update
  }
  isInitialMount.current = false;
});

hrdwdmrbl
  • 4,814
  • 2
  • 32
  • 41
-3

Use the Cleanup function of the useEffect without using an empty array as a second parameter:

useEffect(() => { 
  return () => {
  // your code to be run on update only.
  }
});

You can use another useEffect (with an empty array as a second parameter) for initial mount, where you place your code in its main function.
  • I'm getting infinite looping – Isaac Pak Aug 15 '21 at 18:57
  • The react cleanup function is executed when component unmounts. The OP is asking how to execute code on "update", or in other words, on every render except the initial render, which is run when mounting. – lance.dolan Oct 04 '21 at 03:26