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.
This commit is contained in:
Raghuraj Pratap Singh
2025-10-01 15:31:38 +05:30
parent fa0879e3a0
commit f08a10eefc
3 changed files with 17 additions and 6 deletions

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ infra/terraform/.terraform/
packages/ios/FormbricksSDK/FormbricksSDK.xcodeproj/project.xcworkspace/xcuserdata
.cursorrules
i18n.cache
formbricks-quickstart/docker-compose.yml

View File

@@ -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<HTMLElement | HTMLDivElement | null>,
@@ -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);

View File

@@ -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<HTMLElement | null>,
@@ -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);