From 02530b86f307737233a2a2acee84e0abd0196aee Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:59:08 +0530 Subject: [PATCH] perf: timezone select component (#26980) * perf: timezone select component * fix: type error * fix: type error * fix: use object assign * fix: use data. length * fix: use props.data reference instead of length for memoization The useMemo dependency on props.data?.length could return stale timezone options when the array contents change but the length stays the same. Using the full props.data reference ensures updates propagate correctly. Fix identified by Cubic AI (confidence 9/10) Co-Authored-By: unknown <> * fix: use data. length * fix: use props.data reference instead of length for memoization The useMemo dependency on props.data?.length could return stale timezone options when the array contents change but the length stays the same. Using the full props.data reference ensures updates propagate correctly. Fix identified by Cubic AI (confidence 9/10) Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../timezone-select/TimezoneSelect.tsx | 118 ++++++++++-------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/packages/features/components/timezone-select/TimezoneSelect.tsx b/packages/features/components/timezone-select/TimezoneSelect.tsx index dae37c6553..c6d32eb22d 100644 --- a/packages/features/components/timezone-select/TimezoneSelect.tsx +++ b/packages/features/components/timezone-select/TimezoneSelect.tsx @@ -1,15 +1,14 @@ "use client"; -import { useMemo, useState } from "react"; -import type { ITimezoneOption, ITimezone, Props as SelectProps } from "react-timezone-select"; -import BaseSelect from "react-timezone-select"; - import { CALCOM_VERSION } from "@calcom/lib/constants"; -import { filterBySearchText, addTimezonesToDropdown, handleOptionLabel } from "@calcom/lib/timezone"; import type { Timezones } from "@calcom/lib/timezone"; +import { addTimezonesToDropdown, filterBySearchText, handleOptionLabel } from "@calcom/lib/timezone"; import { trpc } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; import { getReactSelectProps, inputStyles } from "@calcom/ui/components/form"; +import { useCallback, useMemo, useState } from "react"; +import type { ITimezone, ITimezoneOption, Props as SelectProps } from "react-timezone-select"; +import BaseSelect from "react-timezone-select"; const SELECT_SEARCH_DATA: Timezones = [ { label: "San Francisco", timezone: "America/Los_Angeles" }, @@ -48,15 +47,18 @@ export function TimezoneSelect(props: TimezoneSelectProps) { trpc: { context: { skipBatch: true } }, } ); - const cityTimezonesFormatted = data.map(({ city, timezone }) => ({ label: city, timezone })); - return ( - + const cityTimezonesFormatted = useMemo( + () => data.map(({ city, timezone }) => ({ label: city, timezone })), + [data] ); + + const combinedData = useMemo( + () => [...cityTimezonesFormatted, ...SELECT_SEARCH_DATA], + [cityTimezonesFormatted] + ); + + return ; } export type TimezoneSelectComponentProps = SelectProps & { @@ -83,7 +85,8 @@ export function TimezoneSelectComponent({ isWebTimezoneSelect = true, ...props }: TimezoneSelectComponentProps) { - const data = [...(props.data || [])]; + const data = useMemo(() => props.data || [], [props.data]); + /* * we support multiple timezones for the different labels * e.g. 'Sao Paulo' and 'Brazil Time' both being 'America/Sao_Paulo' @@ -92,15 +95,54 @@ export function TimezoneSelectComponent({ * We make sure to be able to search through both options, and flip the key/value on final display. */ const [additionalTimezones, setAdditionalTimezones] = useState([]); - const handleInputChange = (searchText: string) => { - if (data) setAdditionalTimezones(filterBySearchText(searchText, data)); - }; - const reactSelectProps = useMemo(() => { - return getReactSelectProps({ - components: components || {}, - }); - }, [components]); + const handleInputChange = useCallback( + (searchText: string) => { + if (data) setAdditionalTimezones(filterBySearchText(searchText, data)); + }, + [data] + ); + + const reactSelectProps = useMemo(() => getReactSelectProps({ components: components || {} }), [components]); + + const timezones = useMemo( + () => ({ + ...(data.length > 0 ? addTimezonesToDropdown(data) : {}), + ...(isWebTimezoneSelect ? addTimezonesToDropdown(additionalTimezones) : {}), + }), + [data, isWebTimezoneSelect, additionalTimezones] + ); + + const handleChange = useCallback( + (selectedOption: ITimezoneOption | null) => { + if (!props.onChange) return; + + if (!selectedOption) { + props.onChange(selectedOption as unknown as ITimezoneOption); + return; + } + + const corrections: Record = { + "America/Port_Of_Spain": "America/Port_of_Spain", + "Africa/Porto-novo": "Africa/Porto-Novo", + "Africa/Dar_Es_Salaam": "Africa/Dar_es_Salaam", + }; + + const correctedValue = corrections[selectedOption.value] || selectedOption.value; + props.onChange({ ...selectedOption, value: correctedValue }); + }, + [props.onChange] + ); + + const formatOption = useCallback( + (option: unknown) =>

{(option as ITimezoneOption).value.replace(/_/g, " ")}

, + [] + ); + + const getLabel = useCallback( + (option: unknown) => handleOptionLabel(option as ITimezoneOption, additionalTimezones), + [additionalTimezones] + ); return ( Object.assign({}, base, { @@ -128,28 +167,9 @@ export function TimezoneSelectComponent({ }} onInputChange={handleInputChange} {...props} - onChange={(selectedOption) => { - if (!props.onChange) return; - - if (!selectedOption) { - props.onChange(selectedOption); - return; - } - - // Fix inconsistent timezone naming formats - const corrections: Record = { - "America/Port_Of_Spain": "America/Port_of_Spain", - "Africa/Porto-novo": "Africa/Porto-Novo", - "Africa/Dar_Es_Salaam": "Africa/Dar_es_Salaam", - }; - - const correctedValue = corrections[selectedOption.value] || selectedOption.value; - props.onChange({ ...selectedOption, value: correctedValue }); - }} - formatOptionLabel={(option) => ( -

{(option as ITimezoneOption).value.replace(/_/g, " ")}

- )} - getOptionLabel={(option) => handleOptionLabel(option as ITimezoneOption, additionalTimezones)} + onChange={handleChange} + formatOptionLabel={formatOption} + getOptionLabel={getLabel} classNames={{ ...timezoneClassNames, input: (state) => @@ -175,8 +195,8 @@ export function TimezoneSelectComponent({ ? "p-1 h-fit" : "px-3 h-fit" : size === "sm" - ? "h-7 px-2" - : "h-9 py-0 px-3", + ? "h-7 px-2" + : "h-9 py-0 px-3", props.isDisabled && "bg-subtle", "rounded-[10px]", timezoneClassNames?.control && timezoneClassNames.control(state)