From 229aeeb0c9051ccc153c6d8b23a908f13ffc1ae2 Mon Sep 17 00:00:00 2001 From: Sangboak Lee Date: Sat, 11 Mar 2023 09:24:01 +0900 Subject: [PATCH] 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 Co-authored-by: zomars --- .../components/booking/pages/BookingPage.tsx | 49 ++++++++++++++----- apps/web/lib/parseDate.ts | 16 +++--- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index a0873bc9e1..6ebce26f45 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -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 = ({
{isClientTimezoneAvailable && (rescheduleUid || !eventType.recurringEvent?.freq) && - `${parseDate(date, i18n)}`} + `${parseDateFunc(date)}`} {isClientTimezoneAvailable && !rescheduleUid && eventType.recurringEvent?.freq && @@ -537,7 +564,7 @@ const BookingPage = ({ {isClientTimezoneAvailable && typeof booking.startTime === "string" && - parseDate(dayjs(booking.startTime), i18n)} + parseDateFunc(dayjs(booking.startTime))}

)} diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts index 5da576358e..54b34fbbaf 100644 --- a/apps/web/lib/parseDate.ts +++ b/apps/web/lib/parseDate.ts @@ -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())];