You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React StrictMode下无清理外部库致useEffect重复执行的解决方案探讨

无清理方法的外部库在Next.js StrictMode开发模式下的问题与解决方案

问题场景与示例

以下是Next.js项目中的示例代码:

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 () => {
      // 希望执行清理操作,但外部库未提供对应方法
      // 手动清理需了解库内部实现,存在风险
      //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>
}

上述代码中,complexExternalLibraryFunctionWithoutCleanup代表一个无清理方法、不具备幂等性的外部JS库——重复调用会导致回调被多次注册,触发时执行多次,不符合预期。

在开发模式+StrictMode下,点击"Click me after typing something!"按钮后,回调会被注册两次,导致计数翻倍;但生产环境运行正常。

疑问与复现信息

该问题仅在Next.js项目中复现,纯React示例无法复现。

Next.js复现所需文件

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,
}

启动命令

  • 开发模式:
npm install
npm run dev
  • 生产模式:
npm run build
npm run start

纯React复现尝试(未成功)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.14.7/babel.min.js"></script>
</head>
<body>
<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>

核心问题

  1. 此类无清理、非幂等的外部库,是否根本无法安全用于React?(有评论指出生产环境中组件也可能多次运行,比如用户点击浏览器后退按钮时的卸载重挂载)
  2. 还是StrictMode开发模式过于严苛,在此场景下无法使用?

此外,常见的useRef标记初始化状态的解决方案,被指出在React 19中可能失效。

解决方案分析

关于库的可用性

这类库并非完全不能在React中安全使用,但需要手动处理幂等性和清理逻辑:

  • 生产环境中组件确实可能因为路由跳转、状态更新导致的卸载重挂载,触发重复调用,StrictMode只是提前暴露了这个隐患,并非过于严苛。

可行解决方法

  1. 用ref标记初始化状态,避免重复调用
    新增一个ref来记录是否已经执行过初始化,修改后的useEffect如下:

    useEffect(() => {
      const isInitialized = useRef(false);
      if (ref.current && !isInitialized.current) {
        complexExternalLibraryFunctionWithoutCleanup(ref.current, incIt);
        isInitialized.current = true;
      }
      // 如果能逆向实现清理逻辑,在这里补充
      return () => {
        // 示例中可以手动移除点击监听
        if (ref.current) {
          ref.current.removeEventListener('click', incIt);
        }
      };
    }, []);
    

    注意:手动清理需要了解库的内部实现,存在一定风险,若库的逻辑复杂,可能无法完全模拟清理。

  2. 临时禁用StrictMode(不推荐)
    可以在next.config.js中关闭reactStrictMode,但这会失去StrictMode提前发现隐患的能力,可能导致生产环境出现未知问题。

  3. 封装库的调用逻辑
    将外部库的调用封装成自定义Hook,在Hook内部处理幂等性和清理,确保组件中调用时不会重复执行。

React 19的兼容性问题

React 19中StrictMode的卸载重挂载模拟逻辑有调整,但核心思路依然是保证初始化逻辑只执行一次。可以改用useState配合依赖数组,或者使用React 19新增的useEffectEvent来分离回调逻辑,避免重复绑定。


内容的提问来源于stack exchange,提问作者Ciro Santilli OurBigBook.com

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 11:44:57