fix: resolve hydration mismatch in analytics app installation (#23877)

- Wrap ConfigureStepCard content with NoSSR component
- Prevents blank step 3 configuration page in production
- Fixes hydration mismatch between server and client rendering
- Ensures analytics apps (GTM, Fathom, GA4) installation works properly
- Uses Cal.com's established NoSSR pattern for client-side only rendering

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Anik Dhabal Babu
2025-09-17 20:09:28 +05:30
committed by GitHub
co-authored by Devin AI
parent c2a44e9319
commit 81bd8033a9
@@ -7,6 +7,7 @@ import { useFieldArray, useFormContext } from "react-hook-form";
import { useForm } from "react-hook-form";
import { z } from "zod";
import NoSSR from "@calcom/lib/components/NoSSR";
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { LocationObject } from "@calcom/lib/location";
@@ -188,7 +189,7 @@ const EventTypeGroup = ({
);
};
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
const ConfigureStepCardContent: FC<ConfigureStepCardProps> = (props) => {
const { loading, formPortalRef, handleSetUpLater } = props;
const { t } = useLocale();
const { control, watch } = useFormContext<TEventTypesForm>();
@@ -221,60 +222,69 @@ export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
}
}, [submit, allUpdated, mainForSubmitRef]);
return (
formPortalRef?.current &&
createPortal(
<div className="mt-8">
{fields.map((group, groupIndex) => (
<div key={group.fieldId}>
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
<div className="mb-2 mt-4 flex items-center">
<Avatar
alt=""
imageSrc={group.image} // if no image, use default avatar
size="md"
className="inline-flex justify-center"
/>
<p className="text-subtle block pl-2">{group.slug}</p>
</div>
)}
<EventTypeGroup
groupIndex={groupIndex}
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
submitRefs={submitRefs}
{...props}
/>
</div>
))}
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
Save
</button>
<Button
className="text-md mt-6 w-full justify-center"
type="button"
data-testid="configure-step-save"
onClick={() => {
submitRefs.current.forEach((ref) => ref?.click());
setSubmit(true);
}}
loading={loading}>
{t("save")}
</Button>
if (!formPortalRef?.current) {
return null;
}
<div className="flex w-full flex-row justify-center">
<Button
color="minimal"
data-testid="set-up-later"
onClick={(event) => {
event.preventDefault();
handleSetUpLater();
}}
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
{t("set_up_later")}
</Button>
return createPortal(
<div className="mt-8">
{fields.map((group, groupIndex) => (
<div key={group.fieldId}>
{eventTypeGroups[groupIndex].eventTypes.some((eventType) => eventType.selected === true) && (
<div className="mb-2 mt-4 flex items-center">
<Avatar
alt=""
imageSrc={group.image} // if no image, use default avatar
size="md"
className="inline-flex justify-center"
/>
<p className="text-subtle block pl-2">{group.slug}</p>
</div>
)}
<EventTypeGroup
groupIndex={groupIndex}
setUpdatedEventTypesStatus={setUpdatedEventTypesStatus}
submitRefs={submitRefs}
{...props}
/>
</div>
</div>,
formPortalRef?.current
)
))}
<button form="outer-event-type-form" type="submit" className="hidden" ref={mainForSubmitRef}>
Save
</button>
<Button
className="text-md mt-6 w-full justify-center"
type="button"
data-testid="configure-step-save"
onClick={() => {
submitRefs.current.forEach((ref) => ref?.click());
setSubmit(true);
}}
loading={loading}>
{t("save")}
</Button>
<div className="flex w-full flex-row justify-center">
<Button
color="minimal"
data-testid="set-up-later"
onClick={(event) => {
event.preventDefault();
handleSetUpLater();
}}
className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
{t("set_up_later")}
</Button>
</div>
</div>,
formPortalRef.current
);
};
export const ConfigureStepCard: FC<ConfigureStepCardProps> = (props) => {
return (
<NoSSR>
<ConfigureStepCardContent {...props} />
</NoSSR>
);
};