According to the docs:
componentDidUpdate() is invoked immediately after updating occurs. This method is not called for the initial render.
We can use the new useEffect()
hook to simulate componentDidUpdate()
, but it seems like useEffect()
is being ran after every render, even the first time. How do I get it to not run on initial render?
As you can see in the example below, componentDidUpdateFunction
is printed during the initial render but componentDidUpdateClass
was not printed during the initial render.
function ComponentDidUpdateFunction() { const [count, setCount] = React.useState(0); React.useEffect(() => { console.log("componentDidUpdateFunction"); }); return (
componentDidUpdateFunction: {count} times
componentDidUpdateClass: {this.state.count} times
count
?
We can use the useRef
hook to store any mutable value we like, so we could use that to keep track of if it's the first time the useEffect
function is being run.
If we want the effect to run in the same phase that componentDidUpdate
does, we can use useLayoutEffect
instead.
Example
const { useState, useRef, useLayoutEffect } = React; function ComponentDidUpdateFunction() { const [count, setCount] = useState(0); const firstUpdate = useRef(true); useLayoutEffect(() => { if (firstUpdate.current) { firstUpdate.current = false; return; } console.log("componentDidUpdateFunction"); }); return (
componentDidUpdateFunction: {count} times
You can turn it into custom hooks, like so:
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
if (didMount.current) func();
else didMount.current = true;
}, deps);
}
export default useDidMountEffect;
Usage example:
import React, { useState, useEffect } from 'react';
import useDidMountEffect from '../path/to/useDidMountEffect';
const MyComponent = (props) => {
const [state, setState] = useState({
key: false
});
useEffect(() => {
// you know what is this, don't you?
}, []);
useDidMountEffect(() => {
// react please run me if 'key' changes, but not on initial render
}, [state.key]);
return (
<div>
...
</div>
);
}
// ...
I made a simple useFirstRender
hook to handle cases like focussing a form input:
import { useRef, useEffect } from 'react';
export function useFirstRender() {
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);
return firstRender.current;
}
It starts out as true
, then switches to false
in the useEffect
, which only runs once, and never again.
In your component, use it:
const firstRender = useFirstRender();
const phoneNumberRef = useRef(null);
useEffect(() => {
if (firstRender || errors.phoneNumber) {
phoneNumberRef.current.focus();
}
}, [firstRender, errors.phoneNumber]);
For your case, you would just use if (!firstRender) { ...
.
firstRender
to the array of dependencies in the useEffect, this will run twice (the first time, and when firstRender is set to false) when mounting. I removed it from the dependencies in my code and it worked.
if
s are for though :)
Same approach as Tholle's answer, but using useState
instead of useRef
.
const [skipCount, setSkipCount] = useState(true);
...
useEffect(() => {
if (skipCount) setSkipCount(false);
if (!skipCount) runYourFunction();
}, [dependencies])
EDIT
While this also works, it involves updating state which will cause your component to re-render. If all your component's useEffect
calls (and also all of its children's) have a dependency array, this doesn't matter. But keep in mind that any useEffect
without a dependency array (useEffect(() => {...})
will be run again.
Using and updating useRef
will not cause any re-renders.
@ravi, yours doesn't call the passed-in unmount function. Here's a version that's a little more complete:
/** * Identical to React.useEffect, except that it never runs on mount. This is * the equivalent of the componentDidUpdate lifecycle function. * * @param {function:function} effect - A useEffect effect. * @param {array} [dependencies] - useEffect dependency list. */ export const useEffectExceptOnMount = (effect, dependencies) => { const mounted = React.useRef(false); React.useEffect(() => { if (mounted.current) { const unmount = effect(); return () => unmount && unmount(); } else { mounted.current = true; } }, dependencies); // Reset on unmount for the next mount. React.useEffect(() => { return () => mounted.current = false; }, []); };
useEffect(() => {...});
dependencies
parameter when you call it.
useEffect
. It's not doing anything. That ref you're resetting is going straight in the garbage. If there's a "next mount" it will happen to a different instance of the hook with a mounted
ref all its own, initialized to false.
const unmount = effect(); return () => unmount && unmount();
can be simplified to return effect()
This is the best implementation I've created so far using typescript
. Basically, the idea is the same, using the Ref
but I'm also considering the callback returned by useEffect
to perform cleanup on component unmount.
import {
useRef,
EffectCallback,
DependencyList,
useEffect
} from 'react';
/**
* @param effect
* @param dependencies
*
*/
export default function useNoInitialEffect(
effect: EffectCallback,
dependencies?: DependencyList
) {
//Preserving the true by default as initial render cycle
const initialRender = useRef(true);
useEffect(() => {
let effectReturns: void | (() => void) = () => {};
// Updating the ref to false on the first render, causing
// subsequent render to execute the effect
if (initialRender.current) {
initialRender.current = false;
} else {
effectReturns = effect();
}
// Preserving and allowing the Destructor returned by the effect
// to execute on component unmount and perform cleanup if
// required.
if (effectReturns && typeof effectReturns === 'function') {
return effectReturns;
}
return undefined;
}, dependencies);
}
You can simply use it, as usual as you use the useEffect
hook but this time, it won't run on the initial render. Here is how you can use this hook.
useuseNoInitialEffect(() => {
// perform something, returning callback is supported
}, [a, b]);
If you use ESLint and want to use the react-hooks/exhaustive-deps rule for this custom hook:
{
"rules": {
// ...
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useNoInitialEffect"
}]
}
}
return effect()
?
Keep it simple:
function useEffectAfterFirstRender(effect, deps) {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) isFirstRender.current = false;
else return effect();
}, deps);
}
Other solutions here reduce to this if you remove unnecessary complications:
We need to pass the return value of effect(), because it might be a destructor, but we don't need to do any conditional logic to determine if it is or isn't. Just pass it on, whatever it is, and let useEffect figure it out.
There's no point in resetting isFirstRender to true on unmount, because 1) the condition hasn't become true, and 2) on unmount, that ref is going in the incinerator. It doesn't get reused on the "next mount." There is no next mount. Unmount is death.
Here it is as a complete typescript module:
import { useEffect, useRef, EffectCallback, DependencyList } from 'react';
function useEffectAfterFirstRender(effect: EffectCallback, deps: DependencyList): void {
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) isFirstRender.current = false;
else return effect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export default useEffectAfterFirstRender;
And I upvote Kiran Maniya's suggestion to give it an exhaustive-deps eslint rule:
{
"rules": {
"react-hooks/exhaustive-deps": ["warn", {
"additionalHooks": "useEffectAfterFirstRender"
}]
}
}
a simple way is to create a let
, out of your component and set in to true.
then say if its true set it to false then return (stop) the useEffect function
like that:
import { useEffect} from 'react';
//your let must be out of component to avoid re-evaluation
let isFirst = true
function App() {
useEffect(() => {
if(isFirst){
isFirst = false
return
}
//your code that don't want to execute at first time
},[])
return (
<div>
<p>its simple huh...</p>
</div>
);
}
its Similar to @Carmine Tambasciabs solution but without using state :)
@MehdiDehghani, your solution work perfectly fine, one addition you have to do is on unmount, reset the didMount.current
value to false
. When to try to use this custom hook somewhere else, you don't get cache value.
import React, { useEffect, useRef } from 'react';
const useDidMountEffect = (func, deps) => {
const didMount = useRef(false);
useEffect(() => {
let unmount;
if (didMount.current) unmount = func();
else didMount.current = true;
return () => {
didMount.current = false;
unmount && unmount();
}
}, deps);
}
export default useDidMountEffect;
false
.
didMount.current
to false every time the deps change, and thus never fire the effect! Am I wrong? That didMount
reset, if we want it at all, would need to live in a separate useEffect
whose deps are []
Simplified implementation
import { useRef, useEffect } from 'react';
function MyComp(props) {
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
myProp = 'some val';
};
}, [props.myProp])
return (
<div>
...
</div>
)
}
I thought creating a custom hook would be overkill and I didn't want to muddle my component's readability by using the useLayoutEffect
hook for something unrelated to layouts, so, in my case, I simply checked to see if the value of my stateful variable selectedItem
that triggers the useEffect
callback is its original value in order to determine if it's the initial render:
export default function MyComponent(props) {
const [selectedItem, setSelectedItem] = useState(null);
useEffect(() => {
if(!selectedItem) return; // If selected item is its initial value (null), don't continue
//... This will not happen on initial render
}, [selectedItem]);
// ...
}
selectedItem
becomes null
again," then yes, you need to either A) make sure it never gets set to null
again, or B) initialize useState()
with a value other than null
, something you know it will never be set to again (i.e., -1
).
If you want to skip the first render, you can create a state "firstRenderDone" and set it to true in the useEffect with empty dependecy list (that works like a didMount). Then, in your other useEffect, you can check if the first render was already done before doing something.
const [firstRenderDone, setFirstRenderDone] = useState(false);
//useEffect with empty dependecy list (that works like a componentDidMount)
useEffect(() => {
setFirstRenderDone(true);
}, []);
// your other useEffect (that works as componetDidUpdate)
useEffect(() => {
if(firstRenderDone){
console.log("componentDidUpdateFunction");
}
}, [firstRenderDone]);
firstRenderDone
out of the deps arrray, this will work.
useRef
-based solutions because your first render immediately triggers the second one. So you get a componentDidUpdate
event right away, but all that's actually updating is the variable you're using to avoid triggering an update right away.
All previous are good, but this can be achieved in a simplier way considering that the action in useEffect can be "skipped" placing an if condition(or any other ) that is basically not run first time, and still with the dependency.
For example I had the case of :
Load data from an API but my title has to be "Loading" till the date were not there, so I have an array, tours that is empty at beginning and show the text "Showing" Have a component rendered with different information from those API. The user can delete one by one those info, even all making the tour array empty again as the beginning but this time the API fetch is been already done Once the tour list is empty by deleting then show another title.
so my "solution" was to create another useState to create a boolean value that change only after the data fetch making another condition in useEffect true in order to run another function that also depend on the tour length.
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
here my App.js
import React, { useState, useEffect } from 'react'
import Loading from './Loading'
import Tours from './Tours'
const url = 'API url'
let newTours
function App() {
const [loading, setLoading ] = useState(true)
const [tours, setTours] = useState([])
const [isTitle, isSetTitle] = useState(false)
const [title, setTitle] = useState("Our Tours")
const newTitle = "Tours are empty"
const removeTours = (id) => {
newTours = tours.filter(tour => ( tour.id !== id))
return setTours(newTours)
}
const changeTitle = (title) =>{
if(tours.length === 0 && loading === false){
setTitle(title)
}
}
const fetchTours = async () => {
setLoading(true)
try {
const response = await fetch(url)
const tours = await response.json()
setLoading(false)
setTours(tours)
}catch(error) {
setLoading(false)
console.log(error)
}
}
useEffect(()=>{
fetchTours()
},[])
useEffect(() => {
if (isTitle) {
changeTitle(newTitle)
}else{
isSetTitle(true)
}
}, [tours])
if(loading){
return (
<main>
<Loading />
</main>
)
}else{
return (
<main>
<Tours tours={tours} title={title} changeTitle={changeTitle}
removeTours={removeTours} />
</main>
)
}
}
export default App
Success story sharing
useRef
withuseState
, but using the setter triggered a re-render, which is not happening when assigning tofirstUpdate.current
so I guess this is the only nice way :)componentDidUpdate
with hooks, so that's why I used it.