From f08a10eefcdd491b496c138ee3e43c157303cc04 Mon Sep 17 00:00:00 2001 From: Raghuraj Pratap Singh Date: Wed, 1 Oct 2025 15:31:38 +0530 Subject: [PATCH] fix: prevent TypeError in useClickOutside hook during component unmounting - Add DOM element validation before calling contains() method - Fix race condition where ref.current becomes non-DOM object during React unmounting - Apply fix to both apps/web and packages/surveys useClickOutside hooks - Resolves Sentry issue FORMBRICKS-CLOUD-3WH The issue occurred when React temporarily assigns non-DOM objects to refs during component unmounting, causing "t.contains is not a function" error. The fix adds proper validation to ensure contains() is only called on actual DOM elements. --- .gitignore | 1 + apps/web/lib/utils/hooks/useClickOutside.ts | 11 ++++++++--- packages/surveys/src/lib/use-click-outside-hook.ts | 11 ++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 613e0dfeb3..25da31aa97 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ infra/terraform/.terraform/ packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata .cursorrules i18n.cache +formbricks-quickstart/docker-compose.yml diff --git a/apps/web/lib/utils/hooks/useClickOutside.ts b/apps/web/lib/utils/hooks/useClickOutside.ts index 1702ed9b4b..e1b6abbc4a 100644 --- a/apps/web/lib/utils/hooks/useClickOutside.ts +++ b/apps/web/lib/utils/hooks/useClickOutside.ts @@ -1,5 +1,10 @@ import { RefObject, useEffect } from "react"; +// Helper function to check if a value is a DOM element with contains method +const isDOMElement = (element: any): element is HTMLElement => { + return element && typeof element.contains === "function" && element.nodeType === Node.ELEMENT_NODE; +}; + // Improved version of https://usehooks.com/useOnClickOutside/ export const useClickOutside = ( ref: RefObject, @@ -13,14 +18,14 @@ export const useClickOutside = ( // Do nothing if `mousedown` or `touchstart` started inside ref element if (startedInside || !startedWhenMounted) return; // Do nothing if clicking ref's element or descendent elements - if (!ref.current || ref.current.contains(event.target as Node)) return; + if (!isDOMElement(ref.current) || ref.current.contains(event.target as Node)) return; handler(event); }; const validateEventStart = (event: MouseEvent | TouchEvent) => { - startedWhenMounted = ref.current !== null; - startedInside = ref.current !== null && ref.current.contains(event.target as Node); + startedWhenMounted = isDOMElement(ref.current); + startedInside = isDOMElement(ref.current) && ref.current.contains(event.target as Node); }; document.addEventListener("mousedown", validateEventStart); diff --git a/packages/surveys/src/lib/use-click-outside-hook.ts b/packages/surveys/src/lib/use-click-outside-hook.ts index d79af49a90..d3d84bc7eb 100644 --- a/packages/surveys/src/lib/use-click-outside-hook.ts +++ b/packages/surveys/src/lib/use-click-outside-hook.ts @@ -1,5 +1,10 @@ import { MutableRef, useEffect } from "preact/hooks"; +// Helper function to check if a value is a DOM element with contains method +const isDOMElement = (element: any): element is HTMLElement => { + return element && typeof element.contains === "function" && element.nodeType === Node.ELEMENT_NODE; +}; + // Improved version of https://usehooks.com/useOnClickOutside/ export const useClickOutside = ( ref: MutableRef, @@ -13,14 +18,14 @@ export const useClickOutside = ( // Do nothing if `mousedown` or `touchstart` started inside ref element if (startedInside || !startedWhenMounted) return; // Do nothing if clicking ref's element or descendent elements - if (!ref.current || ref.current.contains(event.target as Node)) return; + if (!isDOMElement(ref.current) || ref.current.contains(event.target as Node)) return; handler(event); }; const validateEventStart = (event: MouseEvent | TouchEvent) => { - startedWhenMounted = ref.current !== null; - startedInside = ref.current !== null && ref.current.contains(event.target as Node); + startedWhenMounted = isDOMElement(ref.current); + startedInside = isDOMElement(ref.current) && ref.current.contains(event.target as Node); }; document.addEventListener("mousedown", validateEventStart);