React useEffect running twice

Shivan M.

The Problem

When developing an application in React 18+, you may encounter an issue where the useEffect hook is being run twice on mount.

The Solution

This occurs because since React 18, when you are in development, your application is being run in StrictMode by default. In Strict Mode, React will try to simulate the behavior of mounting, unmounting, and remounting a component to help developers uncover bugs during testing.

Although this behavior may seem undesirable, or wrong, it exists to help developers ensure their code properly uses the useEffect hook. From this article from the React team, we can see the reasoning behind useEffect running twice:

This illustrates that if remounting breaks the logic of your application, this usually uncovers existing bugs. From the user’s perspective, visiting a page shouldn’t be different from visiting it, clicking a link, and then pressing Back. React verifies that your components don’t break this principle by remounting them once in development.

In most cases, it should be fine to leave your code as-is, since the useEffect will only run once in production. In the case that your application isn’t functioning correctly because it runs twice, you can try the following solutions.

Refactor useEffect So That It Works Correctly After Remounting

The useEffect hook enables you to synchronize with state or external services that live outside the React tree.

Consider the following incorrect usage of the useEffect hook:

"use client"; import { useEffect } from 'react'; export default function MyComponent() { useEffect(() => { if (product.isInCart) { showNotification(`Added ${product.name} to the shopping cart!`); } }, [product]); function handleBuyClick() { addToCart(product); } function handleCheckoutClick() { addToCart(product); navigateTo('/checkout'); } return ( <div> ... </div> ); }

In this example, we want to show a notification when a user puts a product in the cart. In this case, two event handlers encapsulate the addToCart functionality. It might be tempting to consolidate the code that shows the notification in the useEffect; however, this effect is incorrect and will lead to issues.

Suppose that the shopping cart is persisted through page reloads. In this case, when the page is reloaded, the notification will be shown again.

To refactor this function, we should determine why the notification should be shown. In this case, the notification should be shown because the user clicked the button, and not because the component was shown to the user. In general, effects are for code that should run because the component was shown to the user.

Using this logic, we can refactor the component as follows, removing the useEffect:

"use client"; export default function MyComponent() { function buyProduct() { addToCart(product); showNotification(`Added ${product.name} to the shopping cart!`); } function handleBuyClick() { buyProduct(); } function handleCheckoutClick() { buyProduct(); navigateTo('/checkout'); } return ( <div> ... </div> ); }

The React documentation provides an extensive article on where and how to use the useEffect hook correctly.

Clean Up After the useEffect Hook

Consider the following component:

"use client"; import { useEffect } from 'react'; export default function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(() => { setCount(count + 1); }, 1000); }, [count]) return ( <div> <h1>Count: {count}</h1> </div> ); }

In this component, we use the setInterval function to update the count variable every second. However, after the component is unmounted, we don’t clean up. This can cause memory leaks and lead to inaccurate values of count when remounting.

By adding a return statement to useEffect, we can clean up the interval, thereby ensuring that the side effect does not persist through component mounts.

"use client"; import { useEffect } from 'react'; export default function MyComponent() { const [count, setCount] = useState(0); useEffect(() => { const interval = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(interval); }, [count]) return ( <div> <h1>Count: {count}</h1> </div> ); }

Disable Strict Mode

Although it is not recommended, in React 18 you can disable Strict Mode by removing the <React.StrictMode> tag from the return statement in your root component.

In Next.js, you can disable Strict Mode by setting the following parameter in next.config.js:

module.exports = { reactStrictMode: false, }

Additional Reading

Understanding effects in React is integral to correctly using them in your applications and avoiding errors. The React documentation contains useful and deep articles about effects and their usage. You can find several of them below:

Get Started With Sentry

Get actionable, code-level insights to resolve React performance bottlenecks and errors.

  1. Create a free Sentry account

  2. Create a React project and note your DSN

  3. Grab the Sentry React SDK

npm install @sentry/react
  1. Configure your DSN
import React from "react"; import ReactDOM from "react-dom"; import * as Sentry from "@sentry/react"; import App from "./App"; Sentry.init({ dsn: "https://<key>@sentry.io/<project>" }); ReactDOM.render(<App />, document.getElementById("root"));

Check our documentation for the latest instructions.

Loved by over 4 million developers and more than 90,000 organizations worldwide, Sentry provides code-level observability to many of the world’s best-known companies like Disney, Peloton, Cloudflare, Eventbrite, Slack, Supercell, and Rockstar Games. Each month we process billions of exceptions from the most popular products on the internet.

Share on Twitter
Bookmark this page
Ask a questionJoin the discussion

Related Answers

A better experience for your users. An easier life for your developers.

    TwitterGitHubDribbbleLinkedinDiscord
© 2024 • Sentry is a registered Trademark
of Functional Software, Inc.