Fix booking time hydration error (#7253)

Root cause of the error is that both server and client try to parse date differently due to localStorage. Since server does not know localStorage when render initial HTML it renders page as if there is no value set for time format.
 On the other hand, client side know the value of time format when it tries to render.

To fix the issue, we force it to parse date with default format at the initial rendering time.

- Add `withDefaultTimeFormat` argument to `parseDate` function
- Add `withDefaultTimeFormat` argument to `parseRecurringDates` function
- Leverage useEffect to dynamically update `withDefaultTimeFormat` arg passed to function

Co-authored-by: sangboak.lee <sangboak.lee@woowahan.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Sangboak Lee
2023-03-11 00:24:01 +00:00
committed by GitHub
co-authored by sangboak.lee zomars
parent 6a9704349d
commit 229aeeb0c9
2 changed files with 48 additions and 17 deletions
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
import { useEffect, useMemo, useReducer, useState } from "react";
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
@@ -15,6 +15,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import { getEventTypeAppData } from "@calcom/app-store/utils";
import type { LocationObject } from "@calcom/core/location";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
@@ -38,6 +39,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { Button, Form, Tooltip } from "@calcom/ui";
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
@@ -213,6 +215,20 @@ const BookingPage = ({
duration = Number(queryDuration);
}
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
// At initial render, we use the default time format which is 12H
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
const parseDateFunc = useCallback(
(date: string | null | Dayjs) => {
return parseDate(date, i18n, withDefaultTimeFormat);
},
[withDefaultTimeFormat]
);
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
useEffect(() => {
setWithDefaultTimeFormat(false);
}, []);
useEffect(() => {
if (top !== window) {
//page_view will be collected automatically by _middleware.ts
@@ -370,15 +386,26 @@ const BookingPage = ({
// Calculate the booking date(s)
let recurringStrings: string[] = [],
recurringDates: Date[] = [];
const parseRecurringDatesFunc = useCallback(
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
return parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: recurringEvent,
recurringCount: recurringCount,
withDefaultTimeFormat: withDefaultTimeFormat,
},
i18n
);
},
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
);
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
[recurringStrings, recurringDates] = parseRecurringDates(
{
startDate: date,
timeZone: timeZone(),
recurringEvent: eventType.recurringEvent,
recurringCount: parseInt(recurringEventCount.toString()),
},
i18n
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
date,
eventType.recurringEvent,
parseInt(recurringEventCount.toString())
);
}
@@ -507,7 +534,7 @@ const BookingPage = ({
<div className="text-sm font-medium">
{isClientTimezoneAvailable &&
(rescheduleUid || !eventType.recurringEvent?.freq) &&
`${parseDate(date, i18n)}`}
`${parseDateFunc(date)}`}
{isClientTimezoneAvailable &&
!rescheduleUid &&
eventType.recurringEvent?.freq &&
@@ -537,7 +564,7 @@ const BookingPage = ({
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{isClientTimezoneAvailable &&
typeof booking.startTime === "string" &&
parseDate(dayjs(booking.startTime), i18n)}
parseDateFunc(dayjs(booking.startTime))}
</p>
</div>
)}
+10 -6
View File
@@ -3,21 +3,23 @@ import { RRule } from "rrule";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
import type { RecurringEvent } from "@calcom/types/Calendar";
import { parseZone } from "./parseZone";
const processDate = (date: string | null | Dayjs, i18n: I18n) => {
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
const parsedZone = parseZone(date);
if (!parsedZone?.isValid()) return "Invalid date";
const formattedTime = parsedZone?.format(detectBrowserTimeFormat);
const formattedTime = parsedZone?.format(
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
);
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
};
export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
if (!date) return ["No date"];
return processDate(date, i18n);
return processDate(date, i18n, withDefaultTimeFormat);
};
export const parseRecurringDates = (
@@ -26,11 +28,13 @@ export const parseRecurringDates = (
timeZone,
recurringEvent,
recurringCount,
withDefaultTimeFormat,
}: {
startDate: string | null | Dayjs;
timeZone?: string;
recurringEvent: RecurringEvent | null;
recurringCount: number;
withDefaultTimeFormat: boolean;
},
i18n: I18n
): [string[], Date[]] => {
@@ -50,7 +54,7 @@ export const parseRecurringDates = (
});
const dateStrings = times.map((t) => {
// finally; show in local timeZone again
return processDate(t.tz(timeZone), i18n);
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
});
return [dateStrings, times.map((t) => t.toDate())];