Compare commits

..

1 Commits

Author SHA1 Message Date
Matti Nannt 6955d7c99f fix: clean up setTimeout schedules in contact effects
Two useEffect hooks scheduled `setTimeout` callbacks without returning
a cleanup. If the effect re-ran (or the component unmounted) before the
timeout fired, the callback would still execute against stale state /
unmounted refs. Capture the timeout id and return `() => clearTimeout(id)`.

Flagged by react-doctor as react-doctor/effect-needs-cleanup
(State & Effects, error).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:41:43 +02:00
5 changed files with 12 additions and 7 deletions
@@ -38,17 +38,20 @@ const formatArrayToRecord = (responseValue: TResponseDataValue, keys: string[]):
return result;
};
const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
// Export for testing
export const formatAddressData = (responseValue: TResponseDataValue): Record<string, string> => {
const addressKeys = ["addressLine1", "addressLine2", "city", "state", "zip", "country"];
return formatArrayToRecord(responseValue, addressKeys);
};
const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
// Export for testing
export const formatContactInfoData = (responseValue: TResponseDataValue): Record<string, string> => {
const contactInfoKeys = ["firstName", "lastName", "email", "phone", "company"];
return formatArrayToRecord(responseValue, contactInfoKeys);
};
const extractResponseData = (response: TResponseWithQuotas, survey: TSurvey): Record<string, any> => {
// Export for testing
export const extractResponseData = (response: TResponseWithQuotas, survey: TSurvey): Record<string, any> => {
const responseData: Record<string, any> = {};
const elements = getElementsFromBlocks(survey.blocks);
@@ -133,7 +133,7 @@ export const EditContactAttributesModal = ({
const errorFieldId = `attribute-key-${firstErrorIndex}`;
const errorElement = document.getElementById(errorFieldId);
if (errorElement) {
setTimeout(() => {
const timeoutId = setTimeout(() => {
errorElement.scrollIntoView({ behavior: "smooth", block: "center" });
// Try to focus the input inside the combobox if it exists
const inputElement = errorElement.querySelector("input") as HTMLInputElement;
@@ -143,6 +143,7 @@ export const EditContactAttributesModal = ({
errorElement.focus();
}
}, 100);
return () => clearTimeout(timeoutId);
}
}
}
@@ -337,9 +337,10 @@ export const UploadContactsCSVButton = ({
useEffect(() => {
if (error && errorContainerRef.current) {
// Small delay to ensure DOM has updated and the alert is visible
setTimeout(() => {
const timeoutId = setTimeout(() => {
errorContainerRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
}, 100);
return () => clearTimeout(timeoutId);
}
}, [error]);
@@ -17,7 +17,7 @@ interface TemplateTagsProps {
type NonNullabeChannel = NonNullable<TWorkspaceConfigChannel>;
const getRoleBasedStyling = (role: TTemplateRole | undefined): string => {
export const getRoleBasedStyling = (role: TTemplateRole | undefined): string => {
switch (role) {
case "productManager":
return "border-blue-300 bg-blue-50 text-blue-500";
@@ -26,4 +26,4 @@ function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge };
export { Badge, badgeVariants };