Tips for Using React’s UseEffect Effectively
Dependency array, code optimization, useCallback, and more

Hooks are a great way to access React’s state and lifecycle methods from a functional component. The useEffect
Hook is a function (effect
) that runs after render and every time the DOM updates.
In this article, we’ll discuss some tips to better use the useEffect
Hook.
1. Child Effects Fire First
Think of the useEffect
Hook as componentDidMount
, componentDidUpdate
, and componentWillUnmount
combined.
So the useEffect
Hook behaves similarly to the class lifecycle methods. One behaviour to note is that the child callback is fired prior to the parent callback. You can check this behaviour here.
Say you have to trigger a payment automatically. This code is written in the child component that runs after render, but the actual details (total amount, discount, etc.) required for payment is fetched in the parent component’s effect. In this case, as payment is triggered before setting the details required, it won’t be successful.
Bear this in mind and structure your code accordingly.
2. The Dependency Array
Let’s start with the basics. The useEffect
Hook accepts a second argument, known as a dependency array, to control when the callback should fire.
Running effects on every DOM update
Not passing in a dependency array will run the callback on every DOM update.
Running effects on initial render
Passing in an empty array runs the effects only after the initial render. By this time the state is updated with its initial values. Further updates in DOM does not call this effect.
This is similar to the componentDidMount
and componentWillUnmount
(on return
) lifecycle methods.
This is where all the listeners and subscriptions required for your page is added.
Running effects on specific props changes
Suppose you have to fetch data (product details) based on which product a user is interested in — the selected product has a productId
, for example. We need to run the callback every time the productId
changes — not just on the initial render or on every DOM update.
This basically replicated the componentDidUpdate
lifecycle method.
We can also pass multiple values to the dependency array.
A classic counter example will help us better understand this:
In the example above, an update in counter1
or counter2
would trigger the below effect.
Passing objects in dependency array
Now, what if your callback is dependent on an object. Would our effects run successfully if you do this?:
The answer is no — because objects are reference types. Any change in the object’s property would go unnoticed by the dependency array because only the reference is checked but not the values inside.
There are a few approaches that you can follow to perform deep comparisons in objects.
JSON.stringify
the object:
Now, our effect could detect when an object’s property changes and function as expected.
useRef
and Lodash for comparison:
You can also write your own custom functions for comparison using useRef
. It is used to hold mutable values throughout the component's lifetime in its current
property.
You can read more on useRef if you’re not familiar with it.
- External packages
If your object is too complex to do a comparison yourself, you can use npm packages to do it for you. A popular suggestion is use-deep-compare-effect
:
The useDeepCompareEffect
will do a deep comparison and run the callback only when the object obj
has changed.
3. Multiple useEffect
for Cleaner Code
Now that we know about the dependency array, we might need to separate effects to run on different lifecycle events of a component or simply for cleaner code.
As already seen in the demo earlier, it is possible to write multiple useEffect
in a single component.
4. Handle Dependencies in Functions
Suppose you want to break down the code into smaller functions and call it from effect, as below:
This would not give us the expected behaviour — doSomething
is dependent on data
which is not included in the useEffect
array. Any update to data will not fire our callback.
This is why React recommends we have the function within the useEffect
as its easier to trace the dependencies.
But what if you want to write reusable functions? Or pass it down from a parent component?
When you want to pass a function as a prop to its child component, you need to add the function to the dependency array in the child component’s effect. But every time something changes in parent, new instances of these functions are created and our callback is fired. This is inefficient.
For scenarios like these, useCallback
comes to the rescue.
Similar to useEffect
, the useCallback
accepts a callback and an array of dependencies. It will return a memoized version of the callback that only changes its identity if any of the dependencies has changed, ensuring we don't create a new instance of the function every time the parent re-renders.
You can experiment with the above here.
5. Always Call Hooks at the Top Level
You cannot call useEffect
(or any Hooks) inside conditional, loops or nested functions.
You can read more on rules of Hooks.
TL;DR
All the points discussed above are in this code. You can play around with it.
Thanks for reading. Happy coding!