Skip to content

Commit e4d86dd

Browse files
committed
DateTime cache local timezone
1 parent 6ac735c commit e4d86dd

File tree

1 file changed

+71
-48
lines changed

1 file changed

+71
-48
lines changed

apps/webapp/app/components/primitives/DateTime.tsx

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
11
import { GlobeAltIcon, GlobeAmericasIcon } from "@heroicons/react/20/solid";
22
import { Laptop } from "lucide-react";
3-
import { Fragment, type ReactNode, useEffect, useState } from "react";
3+
import { Fragment, type ReactNode, useSyncExternalStore } from "react";
44
import { CopyButton } from "./CopyButton";
55
import { useLocales } from "./LocaleProvider";
66
import { Paragraph } from "./Paragraph";
77
import { SimpleTooltip } from "./Tooltip";
88

9+
// Cache the browser's local timezone - resolved once and reused
10+
let cachedLocalTimeZone: string | null = null;
11+
12+
function getLocalTimeZone(): string {
13+
if (cachedLocalTimeZone === null) {
14+
cachedLocalTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
15+
}
16+
return cachedLocalTimeZone;
17+
}
18+
19+
// For SSR compatibility: returns "UTC" on server, actual timezone on client
20+
function subscribeToTimeZone() {
21+
// No-op - timezone doesn't change
22+
return () => {};
23+
}
24+
25+
function getTimeZoneSnapshot(): string {
26+
return getLocalTimeZone();
27+
}
28+
29+
function getServerTimeZoneSnapshot(): string {
30+
return "UTC";
31+
}
32+
33+
/**
34+
* Hook to get the browser's local timezone.
35+
* Uses useSyncExternalStore for SSR compatibility - returns "UTC" on server,
36+
* actual timezone on client. The timezone is cached and only resolved once.
37+
*/
38+
export function useLocalTimeZone(): string {
39+
return useSyncExternalStore(subscribeToTimeZone, getTimeZoneSnapshot, getServerTimeZoneSnapshot);
40+
}
41+
942
type DateTimeProps = {
1043
date: Date | string;
1144
timeZone?: string;
@@ -28,15 +61,10 @@ export const DateTime = ({
2861
hour12 = true,
2962
}: DateTimeProps) => {
3063
const locales = useLocales();
31-
const [localTimeZone, setLocalTimeZone] = useState<string>("UTC");
64+
const localTimeZone = useLocalTimeZone();
3265

3366
const realDate = typeof date === "string" ? new Date(date) : date;
3467

35-
useEffect(() => {
36-
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
37-
setLocalTimeZone(resolvedOptions.timeZone);
38-
}, []);
39-
4068
const tooltipContent = (
4169
<TooltipContent
4270
realDate={realDate}
@@ -128,38 +156,23 @@ export function formatDateTimeISO(date: Date, timeZone: string): string {
128156
}
129157

130158
// New component that only shows date when it changes
131-
export const SmartDateTime = ({ date, previousDate = null, timeZone = "UTC", hour12 = true }: DateTimeProps) => {
159+
export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: DateTimeProps) => {
132160
const locales = useLocales();
161+
const localTimeZone = useLocalTimeZone();
133162
const realDate = typeof date === "string" ? new Date(date) : date;
134163
const realPrevDate = previousDate
135164
? typeof previousDate === "string"
136165
? new Date(previousDate)
137166
: previousDate
138167
: null;
139168

140-
// Initial formatted values
141-
const initialTimeOnly = formatTimeOnly(realDate, timeZone, locales, hour12);
142-
const initialWithDate = formatSmartDateTime(realDate, timeZone, locales, hour12);
143-
144-
// State for the formatted time
145-
const [formattedDateTime, setFormattedDateTime] = useState<string>(
146-
realPrevDate && isSameDay(realDate, realPrevDate) ? initialTimeOnly : initialWithDate
147-
);
148-
149-
useEffect(() => {
150-
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
151-
const userTimeZone = resolvedOptions.timeZone;
152-
153-
// Check if we should show the date
154-
const showDatePart = !realPrevDate || !isSameDay(realDate, realPrevDate);
169+
// Check if we should show the date
170+
const showDatePart = !realPrevDate || !isSameDay(realDate, realPrevDate);
155171

156-
// Format with appropriate function
157-
setFormattedDateTime(
158-
showDatePart
159-
? formatSmartDateTime(realDate, userTimeZone, locales, hour12)
160-
: formatTimeOnly(realDate, userTimeZone, locales, hour12)
161-
);
162-
}, [locales, realDate, realPrevDate, hour12]);
172+
// Format with appropriate function
173+
const formattedDateTime = showDatePart
174+
? formatSmartDateTime(realDate, localTimeZone, locales, hour12)
175+
: formatTimeOnly(realDate, localTimeZone, locales, hour12);
163176

164177
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
165178
};
@@ -174,7 +187,12 @@ function isSameDay(date1: Date, date2: Date): boolean {
174187
}
175188

176189
// Format with date and time
177-
function formatSmartDateTime(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string {
190+
function formatSmartDateTime(
191+
date: Date,
192+
timeZone: string,
193+
locales: string[],
194+
hour12: boolean = true
195+
): string {
178196
return new Intl.DateTimeFormat(locales, {
179197
month: "short",
180198
day: "numeric",
@@ -189,7 +207,12 @@ function formatSmartDateTime(date: Date, timeZone: string, locales: string[], ho
189207
}
190208

191209
// Format time only
192-
function formatTimeOnly(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string {
210+
function formatTimeOnly(
211+
date: Date,
212+
timeZone: string,
213+
locales: string[],
214+
hour12: boolean = true
215+
): string {
193216
return new Intl.DateTimeFormat(locales, {
194217
hour: "2-digit",
195218
minute: "numeric",
@@ -210,19 +233,14 @@ export const DateTimeAccurate = ({
210233
hour12 = true,
211234
}: DateTimeProps) => {
212235
const locales = useLocales();
213-
const [localTimeZone, setLocalTimeZone] = useState<string>("UTC");
236+
const localTimeZone = useLocalTimeZone();
214237
const realDate = typeof date === "string" ? new Date(date) : date;
215238
const realPrevDate = previousDate
216239
? typeof previousDate === "string"
217240
? new Date(previousDate)
218241
: previousDate
219242
: null;
220243

221-
useEffect(() => {
222-
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
223-
setLocalTimeZone(resolvedOptions.timeZone);
224-
}, []);
225-
226244
// Smart formatting based on whether date changed
227245
const formattedDateTime = hideDate
228246
? formatTimeOnly(realDate, localTimeZone, locales, hour12)
@@ -253,7 +271,12 @@ export const DateTimeAccurate = ({
253271
);
254272
};
255273

256-
function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string {
274+
function formatDateTimeAccurate(
275+
date: Date,
276+
timeZone: string,
277+
locales: string[],
278+
hour12: boolean = true
279+
): string {
257280
const formattedDateTime = new Intl.DateTimeFormat(locales, {
258281
month: "short",
259282
day: "numeric",
@@ -269,21 +292,21 @@ function formatDateTimeAccurate(date: Date, timeZone: string, locales: string[],
269292
return formattedDateTime;
270293
}
271294

272-
export const DateTimeShort = ({ date, timeZone = "UTC", hour12 = true }: DateTimeProps) => {
295+
export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
273296
const locales = useLocales();
297+
const localTimeZone = useLocalTimeZone();
274298
const realDate = typeof date === "string" ? new Date(date) : date;
275-
const initialFormattedDateTime = formatDateTimeShort(realDate, timeZone, locales, hour12);
276-
const [formattedDateTime, setFormattedDateTime] = useState<string>(initialFormattedDateTime);
277-
278-
useEffect(() => {
279-
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
280-
setFormattedDateTime(formatDateTimeShort(realDate, resolvedOptions.timeZone, locales, hour12));
281-
}, [locales, realDate, hour12]);
299+
const formattedDateTime = formatDateTimeShort(realDate, localTimeZone, locales, hour12);
282300

283301
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
284302
};
285303

286-
function formatDateTimeShort(date: Date, timeZone: string, locales: string[], hour12: boolean = true): string {
304+
function formatDateTimeShort(
305+
date: Date,
306+
timeZone: string,
307+
locales: string[],
308+
hour12: boolean = true
309+
): string {
287310
const formattedDateTime = new Intl.DateTimeFormat(locales, {
288311
hour: "numeric",
289312
minute: "numeric",

0 commit comments

Comments
 (0)