Рассмотрим следующий пример:
pages/index.js
Здесь complexExternalLibraryFunctionWithoutCleanup
, как следует из названия, представляет внешнюю библиотеку JavaScript, которая действует на заданный элемент и не имеет метода очистки.
Более того, complexExternalLibraryFunctionWithoutCleanup
не является идемпотентной, потому что если вы вызовете ее дважды, ее обратный вызов сработает дважды при щелчке, что нежелательно.
В режиме разработки + StrictMode
я заметил, что после нажатия кнопки Нажмите меня после ввода чего-либо!
обратный вызов регистрируется дважды.
Однако в режиме производства это работает так, как и ожидалось.
Как правильно предотвратить эту проблему в режиме разработки? Это:
К сожалению, мне удалось воспроизвести проблему только на Next.js, а не на чистом примере React. Должно быть, я что-то упускаю. Остальные файлы Next.js для воспроизведения:
package.json
.eslintrc
next.config.js
а затем для режима разработки:
и режима производства:
Моя неудачная попытка воспроизведения на чистом React:
но по какой-то причине это не выявляет проблему.
Я видел такие вопросы, как Почему useEffect запускается дважды и как правильно с этим справиться в React?, но я хотел бы сосредоточиться конкретно на случае, когда функция очистки недоступна.
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 <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>
}
{
"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"
}
}
{
"extends": "next",
"root": true
}
module.exports = {
reactStrictMode: true,
}
npm install
npm run dev
npm run build
npm run start
<!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>