From fd1f4d311af36984897d8842cc66ee2d3681f8ae Mon Sep 17 00:00:00 2001 From: Paul Aderoju Date: Tue, 15 Sep 2026 22:06:34 +0100 Subject: [PATCH] fix: prevent duplicate iframes on CldUploadWidget unmount/remount When CldUploadWidget unmounts and remounts, next/script's onLoad fires again. The triggerOnIdle callback had no guard against the component unmounting before the idle callback fired, allowing a zombie widget to be created with an orphaned iframe. Add an isMounted ref that gates both handleOnLoad and the idle callback. Set isMounted=true on mount; false + destroy on cleanup, in that order. Closes #587. Supersedes #588. --- .../CldUploadWidget/CldUploadWidget.tsx | 7 ++- .../tests/nextjs-app/app/test-widget/page.tsx | 55 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 next-cloudinary/tests/nextjs-app/app/test-widget/page.tsx diff --git a/next-cloudinary/src/components/CldUploadWidget/CldUploadWidget.tsx b/next-cloudinary/src/components/CldUploadWidget/CldUploadWidget.tsx index 9e5f55a7..58b86ed7 100644 --- a/next-cloudinary/src/components/CldUploadWidget/CldUploadWidget.tsx +++ b/next-cloudinary/src/components/CldUploadWidget/CldUploadWidget.tsx @@ -50,6 +50,7 @@ const CldUploadWidget = ({ const uploadWidgetId = useUploadWidgetId(); const cloudinary: CldUploadWidgetCloudinaryInstance = useRef(); const widget: CldUploadWidgetWidgetInstance = useRef(); + const isMounted = useRef(false); const [error, setError] = useState(undefined); const [results, setResults] = useState(undefined); @@ -130,6 +131,8 @@ const CldUploadWidget = ({ */ function handleOnLoad() { + if ( !isMounted.current ) return; + setIsScriptLoading(false); if ( !cloudinary.current ) { @@ -140,14 +143,16 @@ const CldUploadWidget = ({ // to trigger widget creation. Optional. triggerOnIdle(() => { - if ( !widget.current ) { + if ( isMounted.current && !widget.current ) { widget.current = createWidget(); } }); } useEffect(() => { + isMounted.current = true; return () => { + isMounted.current = false; widget.current?.destroy(); widget.current = undefined; } diff --git a/next-cloudinary/tests/nextjs-app/app/test-widget/page.tsx b/next-cloudinary/tests/nextjs-app/app/test-widget/page.tsx new file mode 100644 index 00000000..6d34636b --- /dev/null +++ b/next-cloudinary/tests/nextjs-app/app/test-widget/page.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useState } from 'react'; +import { CldUploadWidget } from '../../../../'; + +export default function TestWidget() { + const [mounted, setMounted] = useState(true); + const [mountCount, setMountCount] = useState(1); + + function toggle() { + setMounted(false); + setTimeout(() => { + setMounted(true); + setMountCount((c) => c + 1); + }, 300); + } + + return ( +
+

CldUploadWidget — unmount/remount test

+

+ Click Remount widget to unmount and remount the widget. + After each remount, open the browser DevTools → Elements panel and check + that there is only one Cloudinary Upload Widget iframe + in the DOM (look for id starting with cloudinary-widget). +

+

Mount count: {mountCount}

+ +
+ +
+ + {mounted && ( + + {({ open }) => ( + + )} + + )} + +
+

How to verify

+
    +
  1. Open DevTools → Elements (or Inspector)
  2. +
  3. Search for cloudinary-widget in the DOM
  4. +
  5. Click Remount widget several times
  6. +
  7. Confirm there is always exactly one iframe — not one per remount
  8. +
+
+ ); +}