DEV Community

Cover image for New behavior of useEffect in React 18 Dev Strict Mode
Yuwang Cai
Yuwang Cai

Posted on

New behavior of useEffect in React 18 Dev Strict Mode

TL;DR: useEffect will be called twice in React 18 dev strict mode, even with an empty deps list.

We can write a custom hook as a workaround.

❓ Problem

Things started around 2 months ago, when I created a new Next.js app with React 18. I was in the dev mode, writing API fetching stuffs in useEffect hook.

But soon I noticed that my API endpoint was hit twice after every page refresh: I saw two identical requests in the "Network" panel, along with two identical console.log messages in the "Console" panel.

I didn't care much about it at that time, because I thought it was a problem on my side - maybe I wrote some bad codes on my backend or frontend. 😥 There wasn't any bad influence at that time, so I just ignored this issue.

But recently I was writing my own hooks collection, and this strange behavior really confused me a lot. After some searching, I found the reason in the official React 18 blog. (See link below!)

🔬 Previous behavior

Think about this piece of code. What's the result?

import { useEffect, useState } from "react";

export default function App() {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count => count + 1);

  useEffect(() => {
    console.log("Effect is created");
    return () => console.log("Effect is destroyed");
  }, []);

  return <button onClick={increment}>Count: {count}</button>;
Enter fullscreen mode Exit fullscreen mode

The useEffect hook's deps list is empty, so:

  • increment() will only be called once, after the component is mounted, right?
  • The value of count should be 1, right?
  • We will see "Effect is created" in the console, right?

Well, it is true in most cases, but not in React 18 dev strict mode.

In React 18 dev strict mode, we will instead see:

  • increment() is called twice.
  • count is 2.
  • Console:
Effect is created
Effect is destroyed
Effect is created
Enter fullscreen mode Exit fullscreen mode

New behavior screenshot in CodeSandbox

✨ New behavior

Let's see what the React blog said:


React 18 introduces a new development-only check to Strict Mode. This new check will automatically unmount and remount every component, whenever a component mounts for the first time, restoring the previous state on the second mount.


Unmounting and remounting includes:

  • componentDidMount
  • componentWillUnmount
  • useEffect
  • useLayoutEffect
  • useInsertionEffect

That's a brand new behavior! Our component mounts, then unmounts, and then mounts again, along with a second call to useEffect.

For more information, please see Updates to Strict Mode.

🔧 Workaround

What if we really want our useEffect to run only once? Here's a solution:

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

export function useEffectOnce(effect: EffectCallback) {
  // A flag indicating whether the effect has been executed or not.
  const isExecuted = useRef(false);

  useEffect(() => {
    // If executed already, skip it this time.
    if (isExecuted.current) {

    // Otherwise, just call it as normal.

    // And set the flag to true.
    isExecuted.current = true;

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
Enter fullscreen mode Exit fullscreen mode

👆 We can create a custom hook, and use a ref as a flag to indicate "we have already run this callback".

👀 CodeSandbox preview

👋 This is my first article on!

So thank you all for the patience of reading my article! English is not my mother tongue, so I probably wrote something weird above. 😂

And if there's anything wrong about this post, please leave your comment below! 👇 Any suggestions will be appreciated!

Top comments (1)

dreadwood profile image

Thank you for the article, it was helpful