Understanding useEffect, useRef and Custom Hooks
Tags: react, frontend, javascript, beginners
Published: 2022-03-07 16:57:14

---
canonical_url: https://www.perssondennis.com/articles/react-useEffect-render-flow
---

> In this article, we will learn at what time React invokes useEffect, useRef and custom hooks. A usePrevious hook will be used for demonstration.

A question I like to ask developers is "do you understand React's life cycle"? The answer is very often a confident "yes".

Then I show them the code for a _usePrevious_ hook and let them explain why it works. If you don't know what a usePrevious hook is, you can see one below. It's used to get a previous value of a prop or state in a component, see [React docs](https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state).

```react
const usePrevious = (value, defaultValue) => {
  const ref = useRef(defaultValue);

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
};
```

Usually, the answer I get is a diffuse answer mentioning something about useRef updating instantly independent of the life cycle or that useRef doesn't trigger a rerender. That's correct.

Then I ask, "if the useEffect is updating the ref value as soon as the passed in _value_ prop updates, won't the hook return the updated ref value?". The response is most often confusion. Even though my statement is fundamentally wrong, they don't really know React's life cycle well enough to explain what is wrong with my question. In fact, they most often believe that what I am saying is true and stands clueless of why the hook works.

Let us therefore take a look at how the usePrevious hook works. It's a perfect case for explaining how React handles useEffect and useRef.

![Confusion meme](https://www.perssondennis.com/images/articles/react-useEffect-render-flow/react-useEffect-render-flow-confusion.jpg)
_What did you previously say?_

## Logging the Sh\*t Out of usePrevious

Here we have a simple React component, using a usePrevious hook. What it does is to increment a count when a button is clicked. It's an overcomplicated way to do such a thing, we wouldn't really need a usePrevious hook in this case, but since the topic under discussion is the usePrevious hook, the article would be quite boring if we left it out.

```react
// ### App.js
// When the button is clicked, the value is incremented.
// That will in turn increment the count.

// import React, { useEffect, useState } from "react";
// import usePrevious from "./usePrevious";

export default function App() {
  const [value, setValue] = useState(0);
  const [count, setCount] = useState(0);

  const previouseValue = usePrevious(value, 0);

  useEffect(() => {
    if (previouseValue !== value) {
      setCount(count + 1);
    }
  }, [previouseValue, value, count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
}
```

To better understand what React does when running the code, I have the same code here below but with a lot of console logs within it. I will carefully go through them all. You can find the code example at [CodeSandbox](https://codesandbox.io/s/sad-yonath-skhbx6?file=/src/App.js) if you want to elaborate on your own.

```react
// ### App.js (with logs)
// When the button is clicked, the value is incremented.
// That will in turn increment the count.

// import React, { useEffect, useState } from "react";
// import usePrevious from "./usePrevious";

export default function App() {
  const [value, setValue] = useState(0);
  const [count, setCount] = useState(0);

  console.log("[App] rendering App");
  console.log("[App] count (before render):", count);
  console.log("[App] value:", value);
  const previouseValue = usePrevious(value, 0);
  console.log("[App] previousValue:", previouseValue);

  useEffect(() => {
    console.log("[App useEffect] value:", value);
    console.log("[App useEffect] previouseValue:", previouseValue);

    if (previouseValue !== value) {
      console.log("[App useEffect] set count to value:", value, "\n\n");
      setCount(count + 1);
    } else {
      console.log("[App useEffect] not increasing count");
    }
  }, [previouseValue, value, count]);

  console.log("[App] count (after render):", count);
  console.log("[App] done rendering App\n\n");

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setValue(value + 1)}>Increment</button>
    </div>
  );
}

```

```react
// ### usePrevious.js (with logs)

// import { useRef, useEffect } from "react";

const usePrevious = (value, defaultValue) => {
  console.log("[usePrevious] value:", value);
  const ref = useRef(defaultValue);

  useEffect(() => {
    console.log("[usePrevious useEffect] value:", value);
    console.log("[usePrevious useEffect] increment ref.current:", ref.current);
    ref.current = value;
  }, [value]);

  console.log("[usePrevious] ref.current:", ref.current);

  return ref.current;
};

export default usePrevious;

```

Enough of code now, I think. Let's look at what happens when we click the _Increment_ button. Here's what we will see in the output console. I highly recommending opening a second browser window to keep the code visible while you read the rest of this article.

```
# App component renders (1)
[App] rendering App
[App] count (before render): 0
[App] value: 1
[usePrevious] value: 1
[usePrevious] ref.current: 0
[App] previousValue: 0
[App] count (after render): 0
[App] done rendering App

# useEffects run (2)
[usePrevious useEffect] value: 1
[usePrevious useEffect] increment ref.current: 0
[App useEffect] value: 1
[App useEffect] previouseValue: 0
[App useEffect] set count to value: 1

# App component rerenders again (3)
[App] rendering App
[App] count (before render): 1
[App] value: 1
[usePrevious] value: 1
[usePrevious] ref.current: 1
[App] previousValue: 1
[App] count (after render): 1
[App] done rendering App

# useEffects run again (4)
[App useEffect] value: 1
[App useEffect] previouseValue: 1
[App useEffect] not increasing count

# (5)
```

---

**Note:** The description that follows should be treated as an interpretation of the code and output above. It's not the exact algorithm React uses. More about that later.

**(1)** So here's what happens. When we click the increase button, it updates the _value_ state to 1 which triggers a rerender of the App component. The usePrevious hook is the first code to be reached in the rerender, so it gets invoked directly. In that hook, we get the updated prop _value_ of 1 while _ref.current_ is still the default value of 0. React notes that the dependency to useEffect has changed, but it doesn't trigger the useEffect yet. Instead, it returns the _ref.current_ value of 0 from the hook and store it in _previousValue_ variable.

The rendering of the App component continuous and it reaches the useEffect. At this time, _value_ has been updated from 0 to 1, so the useEffect should be triggered, but not yet. Instead of triggering it, React completes its rendering with a default _count_ value of 0.

![Dependencies noted meme](https://www.perssondennis.com/images/articles/react-useEffect-render-flow/react-useEffect-render-flow-dependencies-noted.jpg)
_React notes that a dependency has updated, but does not run the effect immediately_

**(2)** Now, after having completed the rerender of the App component, it's time to run useEffects. React has noted that both the useEffect in usePrevious hook and in App component should be triggered. It starts invoking the useEffect in the usePrevious hook, that's the useEffect that was reached first during rendering.

When it runs the useEffect code it updates the _ref.current_ to 1 and that's all. React continuous with the next useEffect in line, the one in the App component. At the time when the App component was rerendered and React first noticed that a value in the dependency list had updated, the _previousValue_ variable was still set to 0. The reason we triggered the useEffect was because _value_ had incremented from 0 to 1. So, the if-statement comparing _value_ with _previousValue_ will be truthy and we will update the count from 0 to 1.

**(3)** We have now emptied the useEffects queue. No more effects to trigger. React can now check if a rerender is required, and it will notice that it is. _setCount_ has been invoked so the _count_ variable has updated to 1 from 0, so React decides to rerender the component once again.

The state variable _value_ is still 1, we haven't increased that value. This time usePrevious hook gets invoked with the same value as last rendering, so there's no need to trigger the useEffect in the usePrevious hook. _ref.current_ still has a value of 1, so the _previousValue_ variable will be assigned a value of 1. When we then reach the useEffect in App component, React notes that _previousValue_ has updated but does nothing about it. It continues the rerendering of the App component and exits gracefully with a count of 1.

**(4)** Rerendering has completed, but we do have a useEffect in queue to run. As mentioned, the useEffect in usePrevious had no reason to trigger, so React continues directly with the effect in App component. _previousValue_ is now 1, that's why we triggered the useEffect. _value_ hasn't changed though and is still set to 1, so we don't invoke the setCount function.

**(5)** We are now done running the useEffects, so it's time for React to check if a rerender is required again. It isn't though, since neither _value_ or _count_ did update when we ran the effects. So React calms down and waits for further user input.

## What Does the Life Cycle Look Like?

What I did describe above is not a technical description of React's life cycle, rather it's an interpretation of what happens when the code runs. There's no time for a detailed explanation of what the React code really looks like here. It's obviously a bit more advanced than I describe in this article. We would need a more complex example which includes child components etc., and we would need to talk about [render and commit phase](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits). For those who are interested, a brief explanation of that can be found [here](https://techdoma.in/react-16-tutorial/what-are-render-phase-and-commit-phase-in-react).

Anyhow, for the sake of helping you understand the execution order I described in the five steps above, I will summarize it with some pseudocode.

```react
const rerender = () => {
    // run code in component

    // if we reach a useEffect
    if (useEffectDependenciesHasUpdated) {
        useEffectQueue.push(useEffectCode)
    }

    // continue running code in component
}

const reactLifeCycle = () => (
    while (true) {
        if (stateHasChanged) {
            rerender()
            runEffectsInQueue()
        }
    }
)
```

As you can see, the above pseudocode is sufficient to explain why the usePrevious hook works. On a basic level, the life cycle could be expained in this way. React renders a component and runs the code within it. Whenever a useEffect is reached, react looks at its dependency list. If a variable within the dependency list has changed, React adds the callback function in the useEffect to a queue.

Whenever the rerendering has completed, react starts to pop effect callbacks out of that queue and invoke them. When the queue gets empty, React starts checking if it is necessary to rerender any components again.

## Why My Question was Faulty

In the beginning of the article, I explained how I asked people this question about the usePrevious hook. Are you able to explain what it is wrong with the question now?

> _if the useEffect is updating the ref value as soon as the passed in value prop updates, won't the hook return the updated ref value?_

Well, the answer to the question is actually: yes. **If** the useEffect was updating the ref value **as soon** as the passed in value updated, then yes, in that case, we would return the updated ref value. But that's not how React works. The useEffect isn't invoked instantly. It's invoked after React has completed the rendering phase and the parent component already has read the old ref value.

## Conclusion

There are many things to say about React's life cycle handling. In this article we only look at useEffect, useRef and a custom usePrevious hook to see in which order React runs the code.

What we can discover by using a custom usePrevious hook is that React invokes the custom hook as soon as it reaches it during the rendering phase. The hook is merely a piece of code lifted out of the component.

However, at the time we reach a useEffect hook, React seemingly does nothing at all, rather it waits for the component rendering to finish, and then first after that has finished, the callback in the useEffect gets invoked.

I said _seemingly nothing at all_, because it's how it appears to work. Internally React handles many things under the hood. The dependency list must be checked in order to know if we even should run the callback or not. React must also keep track of the old dependencies to be able to compare them. But that's a topic for another day. What you need to know today, is that useEffect callbacks are invoked **after** a component has finished rendering, and they are executed in the same order as the code reaches them.

When a useEffect has run, the component may rerender a second time if its state has updated, e.g. if a set function returned by a useState has been invoked. If a useEffect only updates a useRef value, then React won't rerender the component. That value is updated immediately.

_Thanks for reading,
Dennis_