DEV Community

Cover image for When useEffect runs
Cassidy Williams
Cassidy Williams

Posted on • Edited on • Originally published at cassidoo.co

When useEffect runs

useEffect is one of those React/Preact hooks that most people have a love/hate relationship with, but like it or not, it's good to understand how it works. This is not the first blog post on the subject, and it's certainly not going to be the last, but hopefully I can explain some things to you about when (and why!) it runs in your applications for you to use as a reference!

Please tell my friend what useEffect actually is

Your friend asks a good question! First of all, let's talk about side effects in your applications. When I say side effect, I mean it is something that happens when other things are changing.

For example, if I were to have a very simple add function:

function add(x, y) {
    return x + y
}
Enter fullscreen mode Exit fullscreen mode

I could make a side effect of some other variable changing, like so:

let z = 10
function add(x, y) {
    z = z + x // this is the side effect, it does not change the return value
    return x + y
}
Enter fullscreen mode Exit fullscreen mode

Changing that z variable in here does not change the return value of the function, it's just a side effect of adding x and y!

Now in React/Preact, it's a bit more complex, and side effects aren't always a good thing. And useEffect is "usually" for side effects. Developer David Khourshid aptly said that useEffect should probably have been named useSynchronize, because rather than it being, "an extra thing that should happen when state changes happen," it should be more like, "things that happen to stay in sync with certain state changes."

When does useEffect get called?

So, it does get a little hairy because useEffects behavior has changed a bit across framework updates, but at a high level: it's called on component mount, and whenever anything in the dependency array changes. I'll explain this more deeply!

So using this as our base:

useEffect(() => {
  // your fetch call, changes, etc
  return () => {
    // clean-up
  }
}, [dependencyArray]) // we're staying in sync with this
Enter fullscreen mode Exit fullscreen mode

The dependency array

That second parameter of useEffect is called the dependency array. There's 3 things that can happen here:

  • If the dependency array is empty, useEffect is only called once (note: this has changed in React 18 in development and strict mode because of Suspense things, but this is how it is for Preact and pre-React 18 and I will talk about a workaround later in this post)
  • If it doesn't exist (like it's omitted entirely), then useEffect is called on every state change
  • If it has a variable in it, then useEffect is called when that variable changes

If that dependency array is populated, you can think of the useEffect function as staying "in sync" with the variables in the array.

The return function

Whenever useEffect is about to be called again, or whenever the component is about to be dismounted/destroyed, the "clean-up function" is called.

Or to rephrase, React/Preact calls the clean-up functions when a component unmounts, or when an update is made and it needs to "cancel" the previous effect.

As another, more filled out, example:

useEffect(() => {
  let isCurrent = true
  fetchUser(uid).then((user) => {
    if (isCurrent) setUser(user)
  })
  return () => {
    isCurrent = false
  }
}, [uid])
Enter fullscreen mode Exit fullscreen mode

This might look a little confusing, but the way it works is when the component is mounted, the component will fetch the user.

If uid hasn't changed and the component stays mounted, setUser will be called. If uid changes in that time, isCurrent will be set to false, so setUser won't be called for that out-of-date HTTP call.

Stopping useEffect from being called on mount

Besides controlling the dependency array variables, the only other thing you might want to consider is saying, "hey, I don't want this effect to be called on mount, but ONLY on updates in the dependency array." This is weird but it happens.

For this particular case, you'll want to bring in the useRef hook. I'm not going to explain what that hook does here because that deserves its own blog post (this one is pretty good from Robin Wieruch). Let's assume you have some state variable called syncWithMe that you want to stay in sync with:

const hasMounted = useRef(false);

useEffect(() => {
    if (hasMounted.current) {
        // code here only runs when syncWithMe changes!
    } else {
        hasMounted.current = true;
    }
}, [syncWithMe]);
Enter fullscreen mode Exit fullscreen mode

This is called a "ref flag"! In this example, hasMounted acts as an instance variable that doesn't cause re-renders or effect changes (because it isn't a state variable). So, you set it to true when the component mounts, and then whenever syncWithMe changes, the effect function is called.

Having useEffect called only on mount in React 18+

Because of how the new Suspense functionality works and a bunch of other changes that happened in React 18, useEffect needs to be manipulated more to run just once in development and strict mode (it should be fine in production but eh, this is still worth talking about). It'll look a lot like our previous example, but opposite:

const hasMounted = useRef(false);

useEffect(() => {
  if (hasMounted.current) { return; }

  // do something

  hasMounted.current = true;
}, []);
Enter fullscreen mode Exit fullscreen mode

What if I don't want to use useEffect at all?

Then I think you should probably watch or read some thought-leadery content around why it's bad. Heh.

useEffect isn't bad, it just has its own time and place and a lot of people have Opinions™ about how it should be used. I do recommend watching this talk about useEffect in general. It is titled "Goodbye, useEffect" (once again from David Khourshid, who I referenced above), and explains some nuances of when you should and shouldn't use it.

Hopefully this post was useful for you as a reference!

Top comments (14)

Collapse
 
thethirdrace profile image
Info Comment hidden by post author - thread only accessible via permalink

The title is very misleading as the article doesn't explain when useEffect is called...

Not to mention there are couple points I'm pretty sure are wrong in the article...

When useEffect is called?

The real moment when useEffect is called is after every render.

==> AFTER not during <===

Effects let you run some code after rendering so that you can synchronize your component with some system outside of React.
-- beta.reactjs.org/learn/synchronizi...

But for a better understanding of how things fits together, here's a rundown of how it actually works...

Phase 1: React Render

React renders the component in memory, meaning it executes the function or class component to know what it will produce in output.

Phase 2: React Comparison

There's a comparison phase between React's output, also known as Virtual DOM or VDOM, and the real DOM from the browser.

Phase 3: React Reconciliation/Commit or React Render Opt-out

If the output is the same as the DOM, React will discard the render and won't proceed further with the current change, meaning it will skip the remaining phases. See bailing out of a state update

If the output differs from the DOM, React will pass the necessary instruction to the browser to adjust its DOM. ex: add/remove DOM nodes, modify text, etc. It's during this time that useEffect's callback is scheduled.

Phase 4: React useEffect is run and its callback is scheduled

React will run each useEffect in the order they were registered and schedule their callbacks.

===> This is the WHEN useEffect is run, but not when it's callback is run <===

It's a very important nuance you need to understand, useEffect runs pretty early, but its callback is a deferred event that runs after the browser's render and paint for performance reasons. It's all about the renders.

It's also important to understand that you can change the values in the dependency array however you want, but as long as there's no re-render, the callback given to useEffect will NEVER execute.

Causality can be expressed like this:

  1. State changed
  2. Render
  3. useEffect is fired if React didn't bail out of the render
  4. callback passed to useEffect will be scheduled to run later only if at least 1 value of the dependency array has changed (think of the deferred execution as a requestIdleCallback)

Phase 5: Browser Render

The browser will modify its DOM. This operation happens in memory and everything like CSS will be recalculated during this browser "render" phase

Phase 6: useLayoutEffect

React will run synchronously the useLayoutEffect after the browser's DOM was modified so you can now check nodes size or position or anything that needs the DOM for processing.

===> This hook runs synchronously after the browser render <===

Don't use useLayoutEffect unless you absolutely need it as it will block the JS main thread, which would degrade your app performance.

Phase 7: Browser Painting

The browser will repaint the screen and the user will see the changes.

Phase 8: React useEffect's callback

Once the main thread is "idling", the callbacks will be run in the order they were registered.

Again, this is NOT when useEffect is run, it ran much earlier, it's just the callback that was scheduled that runs at this time.

The callbacks run so late in the cycle because of 2 reasons:

  1. they're supposed to be synchronizing with external systems, meaning they're not important for actual rendering and they can happen later
  2. deferring execution later improves render performance greatly

Notes

All the phases are asynchronous, but are called in a specific order.

Since useEffect's ===> callback <=== is deferred, it's supposed to run last but it's not a guarantee. While it can't happen before running useEffect (phase 4), it could happen as early as before the browser render (phase 5) depending on how the browser optimizes its work. This is why the useLayoutEffect hook was created, to give us a handle when we need to make sure DOM modifications are applied first (although it has performance implications).

Things I believe the article got wrong

So, it does get a little hairy because useEffects behavior has changed a bit across framework updates, but at a high level: it's called on component mount, and whenever anything in the dependency array changes

I'm not aware of any change on when or how the useEffect has been called since its inception. I think it's more of a misconception than anything else. useEffect has always been the same, we just didn't quite catch how it worked...

Also, useEffect is not called on mount, it's called because there was a render which happens to be the first render, aka on mount. It's a small nuance, but it's important to get the difference to better understand the model. It's not the mounting, it's the render that called it.

And every subsequent calls are also because there was a render, not because the dependency array values changed. Granted, you'd need to use pretty weird patterns to not re-render and change a value, but it's possible. Try using a ref in the dependency array and change it, the useEffect will NEVER run after that change because ref will never cause a re-render. Again, it's a small nuance, but it's important to get the difference to better understand the model. It's not the change to the dependency array, it's the render that causes the call to useEffect.

Or to rephrase, React/Preact calls the clean-up functions when a component unmounts, or when an update is made and it needs to "cancel" the previous effect.

Same as with the previous statements. It has nothing to do with mounting, unmounting or updating values, it's all about renders:

There's a render => run clean up function => run `useEffect`
Enter fullscreen mode Exit fullscreen mode

It's all top => down, pretty simple stuff when you think about it.

And 1 more important thing to understand...

===> useEffect will ALWAYS be called after a render, the dependency array is only there to decide if the callback passed to useEffect should be executed or not<===

In other words: useEffect will execute after a render no matter what, the callback passed to useEffect will only execute if values in the dependency array changes.

Because of how the new Suspense functionality works and a bunch of other changes that happened in React 18, useEffect needs to be manipulated more to run just once in development and strict mode

Not really, useEffect is to synchronize stuff. It shouldn't matter that your synchronization runs twice on the first render.

If there's a problem, then your effect has a problem in design. It's also possible you shouldn't use useEffect for that particular thing either...

I highly recommend to read React's official beta documentation, especially the page You might not need an effect

Collapse
 
vanyaxk profile image
Ivan

Great article! Why do useEffects trigger when they use callbacks sent as props from the parent? It does not happen with simple callbacks, like the ones from useState though

Collapse
 
tejasq profile image
Tejas Kumar

It’s because the useEffect does a shallow comparison of the values in its dependency array.

Consider the following scenario:

(() => true) === (() => true)
Enter fullscreen mode Exit fullscreen mode

We’re comparing a function that returns true with another function that returns true. We think they’re the same, but to React, they’re two different functions because they’re both initialized as two distinct functions even though they do the same thing.

If we compare references to functions, then we have equality:

const a = () => true;
const b = () => true;
const c = a;

a === a; // true
a === b; // false
a === c; // true
Enter fullscreen mode Exit fullscreen mode

Comparing references, not values achieves the effect you want.

The callbacks from useState are references.

Another way you can go about this is by wrapping your callbacks in useCallback for even more control.

Collapse
 
vanyaxk profile image
Ivan

Thanks a ton, this explains it!

Thread Thread
 
tejasq profile image
Tejas Kumar

Happy to serve you, Ivan!

Collapse
 
codeofrelevancy profile image
Code of Relevancy

Great article. Thank you for sharing.
The useEffect hook is truly a game-changer, it elegantly combines simplicity and functionality, making it an essential tool for any React developer..

Collapse
 
gene profile image
Gene

Thank you!

Collapse
 
bookercodes profile image
Alex Booker

Brilliant explanation @cassidoo. I love the way you write!

Collapse
 
rishadomar profile image
Rishad Omar

Thanks for this articles and the comments below help my understanding.

Collapse
 
lindiwe09 profile image
Lindiwe

l value the insights and guidance you provide @cassidoo . l love the way you write.

Collapse
 
shiraze profile image
shiraze

Hi, I'm pretty sure the example with let isCurrent = true is incorrect, as isCurrent will be set to true each time the useEffect is fired (after initial render, and whenever uid updates), so will mean that setUser() will be called at each of these times. You could make use of useRef() to make sure it's only called initially, or use the empty dependency array.

Collapse
 
cassidoo profile image
Cassidy Williams

It depends on the fetch timing! If fetch is particularly slow and the uid changes, then the isCurrent will be false for that original fetch call. The functions in useEffect are called in a stack of sorts, with a new isCurrent variable for each one.

Collapse
 
shiraze profile image
shiraze

I thought the fetch promise behaves similar to an enclosure (i.e. the value of inner vars are based on the value of outer vars at the time the function/promise is defined), so I created this codepen to check behaviour: codepen.io/ambience/pen/PodYBqa

You are correct @cassidoo, and we can see from the codepen that if multiple clicks on the button to update uid is made in quick succession, only the last click results in the setUser call being made.

Collapse
 
rei7 profile image
rei

everyone, before you use "ref flag" to bypass react 18+ strict mode running twice, you should know it's actually designed to be a beneficial FEATURE to help catch bugs and find problems. React docs goes to great length talking about it.

Some comments have been hidden by the post's author - find out more