React hooks useEffect only on update?
Asked Answered
A

10

115

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?

Airel answered 9/3, 2019 at 8:53 Comment(2)
Possible duplicate of Make React useEffect hook not run on initial renderMultiplication
Well.. the solution below looks more precise and clear.Airel
C
237

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
  }
});
Consciencestricken answered 9/3, 2019 at 9:21 Comment(4)
I thought that useRef can only be used to DOM manipulation. thanks!Airel
In fact, this question is listed in React FAQ and here here it explicitly says that this is the right way.Expulsion
This is it. I suggest understanding useRef first and then use useUpdateEffect from react-use.Awning
I think strict mode breaks this because of the double render - codesandbox.io/s/sharp-feynman-9qs09sMt
I
50

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);
}
Increscent answered 23/8, 2019 at 20:21 Comment(5)
Why are you disabling the eslint rule?Dentate
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.Increscent
For typescript I had to change to ` useUpdateEffect(effect: Function, dependencies: any[] = [])`Andre
Shouldn't we return effect(); instead of just calling effect(); so that we honor the clean up function?Macarthur
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.Regorge
B
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
};
Brabant answered 3/10, 2019 at 10:24 Comment(2)
Great. Why did we use useIsMounted()? Why shouldn't we use if (!isInitialMount.current) while cleaning up?Cavanagh
Why do we need to set isInitialMount.current = true; when cleaning up? Is that really necessary?Macarthur
I
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
    ...
  })
}

Irreconcilable answered 12/10, 2020 at 10:31 Comment(1)
Isn't the return used for the cleanup on unmount?Illusionist
I
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]);
Isidroisinglass answered 5/3, 2020 at 8:11 Comment(1)
This code won't work because isCartOpen isn't being observed and will always be null.Calci
R
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])
Riggle answered 15/8, 2020 at 7:26 Comment(0)
R
3

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>
Regorge answered 17/2, 2023 at 23:1 Comment(0)
J
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>;
}
Jacintha answered 22/9, 2021 at 8:34 Comment(0)
C
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;
});

Caesarea answered 14/3, 2023 at 21:48 Comment(0)
G
-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.
Gizela answered 18/10, 2020 at 8:38 Comment(2)
I'm getting infinite loopingHaakon
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.Balk

© 2022 - 2024 — McMap. All rights reserved.