Consider the following example:
pages/index.js
import { useState, useRef, useEffect } from 'react'
function complexExternalLibraryFunctionWithoutCleanup(elem, cb) {
elem.addEventListener('click', () => {
cb()
})
}
export default function IndexPage() {
const [text, setText] = useState('')
const [myint, setMyint] = useState(0)
const ref = useRef(null)
function incIt() {
setMyint(i => i+1)
}
useEffect(() => {
if (ref.current) {
complexExternalLibraryFunctionWithoutCleanup(ref.current, incIt)
}
return () => {
// I would like to do a cleanup like this. But it is not provided by the external library.
// So I would have to understand the internals of the library and attempt to manually clean things up,
// which is risky.
//complexExternalLibraryFunctionWithoutCleanup_cleanup(ref.current)
}
}, [])
return <div>
<div><input value={text} onChange={e => setText(e.target.value)} placeholder='Type here' /></div>
<div>You typed: {text}</div>
<div><button ref={ref}>Click me after typing something! If unfixed, it will increment twice!</button></div>
<div><button onClick={incIt}>Click me to increment!</button></div>
<div>{myint}</div>
</div>
}
Here complexExternalLibraryFunctionWithoutCleanup
, as the name suggests, represents an external JavaScript library that acts on a given element, and it has no cleanup method.
Furthermore, complexExternalLibraryFunctionWithoutCleanup
is not idempotent, because if you call it twice its callback runs twice on click which is not desired.
In development + StrictMode
, I observe that after clicking the Click me after typing something!
button the callback does get registered twice.
In production mode however it works as expected.
How to correctly prevent that issue in development mode? Is it:
-
simply never safe to use such a library with React? https://www.reddit.com/r/reactjs/comments/1ewprza/comment/lj0t7hn/ says:
There’s plenty of times where the component will be run multiple times in production. For example if the user uses the browser back button.
so maybe this is the case?
-
or is
StrictMode
development simply too strict, and thus unusable in that case (I can’t have something broken in development-only otherwise it will drive me crazy(-ier))
Unfortunately I was only able to reproduce the issue on Next.js, not on a pure React example, I must be missing something. The rest of the Next.js files for reproduction:
package.json
{
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "eslint ."
},
"dependencies": {
"next": "14.2.5",
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint": "7.24.0",
"eslint-config-next": "14.2.5"
}
}
.eslintrc
{
"extends": "next",
"root": true
}
next.config.js
module.exports = {
reactStrictMode: true,
}
and then for dev mode:
npm install
npm run dev
and production mode:
npm run build
npm run start
My failed attempt at a pure-React reproduction:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
</head>
<body>
<p><a href="https://cirosantilli.com/_file/react/ref-twice.html">https://cirosantilli.com/_file/react/ref-twice.html</a></p>
<div id="root"></div>
<script type="text/babel">
const { StrictMode, useState, useEffect, useRef } = React
function complexExternalLibraryFunctionWithoutCleanup(elem, cb) {
elem.addEventListener('click', () => {
cb()
})
}
function Main(props) {
const [text, setText] = useState('')
const [myint, setMyint] = useState(0)
const ref = useRef(null)
function incIt() {
setMyint(i => i+1)
}
useEffect(() => {
if (ref.current) {
complexExternalLibraryFunctionWithoutCleanup(ref.current, incIt)
}
}, [])
return <div>
<div><input value={text} onChange={e => setText(e.target.value)} placeholder='Type here' /></div>
<div>You typed: {text}</div>
<div><button ref={ref}>Click me after typing something! If unfixed, it will increment twice!</button></div>
<div><button onClick={incIt}>Click me to increment!</button></div>
<div>{myint}</div>
</div>
}
ReactDOM.createRoot(document.getElementById('root')).render(<StrictMode><Main /></StrictMode>)
</script>
</body>
</html>
but that does not exhibit the issue for some reason.
I have seen questions such as Why useEffect running twice and how to handle it well in React? but I’d like to focus specifically on the case where a cleanup function is not available.
Why useEffect running twice and how to handle it well in React? proposes a useRef
approach, but https://stackoverflow.com/a/78443665/895245 says it isn’t supposed to work and will stop working in React 19.
Also asked at: https://www.reddit.com/r/reactjs/comments/1ewprza/how_to_prevent_a_react_useeffect_from_running/
2
Answers
UseEffect is running twice in react’s StrictMode because react wants to catch bugs while you are coding It only happens in development mode so it will not affect your app when it is live.
if you want to stop it from running twice you can use a simple trick You can create a custom hook that skips the first time the effect runs
here’s how
This will prevent the effect from running the first time, so you won’t get that double execution but honestly it’s usually better to let React do its thing and just make sure your code can handle it.
if you want more details you can check out this article: Prevent React from Triggering useEffect Twice.
Without knowing what this external library does, I would say that it is indeed not safe to use it in a
useEffect
. React does not guarantee that the function is auseEffect
is called only once, it could end up being called several times in production, for instance as a race condition in conjunction with concurrent rendering if your user is on a slow device. Or maybe not, but the point is that React does not give any guarantee that it will be called only once, and the different behavior of strict mode is to force you to handle the case when it’s being called several times.Depending on your situation, an alternative solution could be to call this function in an event listener instead (if it is supposed to be triggered on some event). Those are guaranteed to run only once, and don’t require a clean up function.