The most complete guide on using componentDidMount in react hooks
I feel like using componentDidMount in react hooks is quite a mis-understood/confusing topic with varying opinions.
Many believe that supplying an empty dependency array([]
) to the useEffect hook is the equivalent of componentDidMount in react hooks, but whilst close, it is not the exact equivalent and many believe that it should be avoided.
In this article we will go through all the possible solutions in order to solve the componentDidMount react hook issue.
Before we start it is worth noting that there is no exact replacement for componentDidMount.
We will go through, starting with the simplest and then going to the most complex.
Since react hooks have been released, it has been a common issue to compare them to the old class lifecycles, but you probably shouldn’t because it can lead to errors in your code without realising it.
When using react hooks, it is best not to think of the reactjs class lifecycle alternative. Instead try to think about what you need, and how you can achieve it with react hooks.
Solution 1
The first and simplest solution for creating a close componentDidMount equivalent is to provide an empty dependency array like the following:
useEffect(() => {
console.log("Mounted")
}, [])
This is great in really simple use cases, but if you need to have any kind of external dependency in your componentDidMount function, this will not work for you because it can create stale values from previous renders and will otherwise be buggy.
Solution 2
In the case you do have an external dependency, the next thing you should try would be to include the dependency in the dependency array of your useEffect
hook so the effect only runs whenever that dependency changes like in the following example.
const someNumber = 5
useEffect(() => {
console.log("someNumber changed to: ", someNumber)
}, [someNumber])
This is fine until we have complex dependencies, for example if you have a function, for the most part, this will not work and will be run on every render.
The below example is showcasing that problem.
const runOnMount = () => {
console.log("componentDidMount react hook")
}
useEffect(() => {
// this will re-run on every render!
// Be careful!
runOnMount()
}, [runOnMount])
This happens because of something called referential equality. All that basically means in this scenario is that comparing two identical functions will equal false.
And when your react component is re-rendered, the function that is passed into the dependency array, even though it is identical, will be false when compared to the past version.
To get around this we need to save the function as a reference and then compare the reference.
Here is an exmaple of referential equality:
// Does not have referential equality
() => {} === () => {} // false
// Does have referential equality
const a = () => {}
a === a // true
Solution 3
So what should we do if we have a function or an object as a dependency?
The easiest thing to do in this situation (if possible), would be to move the function into the effect like so:
useEffect(() => {
const runOnMount = () => {
console.log("componentDidMount react hooks")
}
runOnMount()
}, [])
But what if you can’t move your function?
Then you move onto solution 4!
Solution 4
If you cannot move your function into the effect, the next option would be to make use of a hook called useCallback
which helps to keep a function identical between renders.
const myMessage = "componentDidMount react hook"
const runOnMount = useCallback(() => {
console.log(myMessage)
}, [myMessage])
useEffect(() => {
runOnMount()
}, [runOnMount])
As you can see we are passing in the function as a dependency to our useEffect
now. This works because the function is now identical between renders meaning the effect will not be triggered unless the dependencies for the function change.
What if I can’t use useCallback
or don’t know the dependencies for the function?
So this is where we need to start getting a little more creative to be able to fix this issue.
Solution 5
Please note, only use these next solutions if none of the above solutions worked for you
Here are two examples of how you could fix this issue if none of the other solutions are an option.
Without a re-render:
import { useEffect, useRef } from "react"
export default function useDidMount(callback) {
const didMount = useRef(null)
useEffect(() => {
if (callback && !didMount.current) {
didMount.current = true
callback()
}
})
}
With a re-render:
import { useEffect, useState } from "react"
export default function useDidMount(callback) {
const [mounted, setMounted] = useState(false)
useEffect(() => {
if (callback && !mounted) {
callback()
setMounted(true)
}
})
}
You can import this hook directly into your project using this npm package I have created: useDidMount
useDidMount code explained
The useEffect callback will be triggered on every render of this hook, but we are conditionally running the callback.
You may have noticed that I did not include the square brackets(the dependency array) at the end of useEffect, like you saw in the first few solutions to the componentDidMount problem.
Adding empty brackets to the second parameter of react hooks useEffect will cause the effect to never be run again and will lead to stale variables and functions if you do not include them, you can read more about this here.
So to get around this, we are allowing the hook to run on every render, and then we conditionally run our code depending on if it has already mounted or not.
By doing this we avoid any confusing side effects caused from improperly configuring the dependency arrays ([]
) and avoid stale variables.
On top of this we have complete control over our hook and when it runs, so you could use this for more than componentDidMount.
Do you need to use componentDidMount with react hooks?
Now, making my way back to my original point at the start, you probably don’t need a componentDidMount equivalent react hook.
After you get used to using react hooks, you will discover that the lifecycles do not exactly match up with react classes lifecycles, and nor should they.
For the most part, you will always want to re-run your hook when a prop changes.
Let me show you an example of what I mean:
export default function SomeComponent({ userProvidedNumber = 5 }) {
const [importantCalculation, setImportantCalculation] = useState(
userProvidedNumber * 2
)
useDidMount(() => {
setImportantCalculation(userProvidedNumber * 2)
})
return <p>{importantCalculation}</p>
}
As you can see the calculation here will only ever be updated onMount, which is an issue if the user provides a new number.
Instead you want to only render when that number changes, and you could do that by using the following:
export default function SomeComponent({ userProvidedNumber = 5 }) {
const [importantCalculation, setImportantCalculation] = useState(
userProvidedNumber * 2
)
useEffect(() => {
setImportantCalculation(userProvidedNumber * 2)
}, [userProvidedNumber])
return <p>{importantCalculation}</p>
}
Now the calculation will always be up to date with what the user has provided.
This is an over simplified example where the effect and state is not actually needed but hopefully it explains the difference.
Basically, what I am trying to say here is that you probably don’t need a useDidMount function, and useEffect will most likely be more relevant, but now in the rare instance you do need it, you now know how to implement it.
Final tips when using the useEffect react hook.
It is worth noting that whenever you use a react hook like useEffect like we did in creating our useDidMount hook, you should always make sure you follow the ESLint rule exhaustive-deps
.
This will tell you in advance if there are errors with your dependency array and help steer you away from issues with useEffect
.
And that is about it for using componentDidMount in react hooks!
I hope this answered all of your questions and that you can now go out and make the best of react hooks - with or without using a componentDidMount equivalent!
You can find the gist with all the solutions here.
You can find the package for solution 5a here.
Will