Shivan M.
—When developing an application in React 18+, you may encounter an issue where the useEffect
hook is being run twice on mount.
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.
useEffect
So That It Works Correctly After RemountingThe 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.
useEffect
HookConsider 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> ); }
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, }
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:
Tasty treats for web developers brought to you by Sentry. Get tips and tricks from Wes Bos and Scott Tolinski.
SEE EPISODESConsidered “not bad” by 4 million developers and more than 100,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.
Here’s a quick look at how Sentry handles your personal information (PII).
×We collect PII about people browsing our website, users of the Sentry service, prospective customers, and people who otherwise interact with us.
What if my PII is included in data sent to Sentry by a Sentry customer (e.g., someone using Sentry to monitor their app)? In this case you have to contact the Sentry customer (e.g., the maker of the app). We do not control the data that is sent to us through the Sentry service for the purposes of application monitoring.
Am I included?We may disclose your PII to the following type of recipients:
You may have the following rights related to your PII:
If you have any questions or concerns about your privacy at Sentry, please email us at [email protected].
If you are a California resident, see our Supplemental notice.