chore: fixing some initial re-renders (#10185)

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
bug/json-input^2
sean-brydon 2023-07-19 10:19:53 +01:00 committed by GitHub
parent 2fd9ef2733
commit 628a29e89c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 113 deletions

View File

@ -1,4 +1,3 @@
import { isEmpty } from "lodash";
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
@ -20,13 +19,13 @@ export function useViewerI18n() {
/**
* Auto-switches locale client-side to the logged in user's preference
*/
const I18nLanguageHandler = (): null => {
const useI18nLanguageHandler = () => {
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale || i18n.language;
useEffect(() => {
// bail early when i18n = {}
if (isEmpty(i18n)) return;
if (Object.keys(i18n).length === 0) return;
// if locale is ready and the i18n.language does != locale - changeLanguage
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
@ -35,8 +34,6 @@ const I18nLanguageHandler = (): null => {
document.dir = i18n.dir();
document.documentElement.setAttribute("lang", locale);
}, [locale, i18n]);
return null;
};
export default I18nLanguageHandler;
export default useI18nLanguageHandler;

View File

@ -13,7 +13,7 @@ import type { AppProps } from "@lib/app-providers";
import AppProviders from "@lib/app-providers";
import { seoConfig } from "@lib/config/next-seo.config";
import I18nLanguageHandler from "@components/I18nLanguageHandler";
import useI18nLanguageHandler from "@components/I18nLanguageHandler";
export interface CalPageWrapper {
(props?: AppProps): JSX.Element;
@ -29,6 +29,7 @@ const calFont = localFont({
});
function PageWrapper(props: AppProps) {
useI18nLanguageHandler();
const { Component, pageProps, err, router } = props;
let pageStatus = "200";
@ -72,7 +73,6 @@ function PageWrapper(props: AppProps) {
}
{...seoConfig.defaultNextSeo}
/>
<I18nLanguageHandler />
<Script
nonce={nonce}
id="page-status"

View File

@ -1,5 +1,5 @@
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { useState, memo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
@ -71,86 +71,91 @@ const format = (date: Date, hour12: boolean) =>
hourCycle: hour12 ? "h12" : "h24",
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
const EventTypeScheduleDetails = ({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const EventTypeScheduleDetails = memo(
({
isManagedEventType,
selectedScheduleValue,
}: {
isManagedEventType: boolean;
selectedScheduleValue: AvailabilityOption | undefined;
}) => {
const { data: loggedInUser } = useMeQuery();
const timeFormat = loggedInUser?.timeFormat;
const { t, i18n } = useLocale();
const { watch } = useFormContext<FormValues>();
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const scheduleId = watch("schedule");
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId:
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
isManagedEventType,
},
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
);
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
const filterDays = (dayNum: number) =>
schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
return (
<div className="border-default space-y-4 rounded border px-6 pb-4">
<ol className="table border-collapse text-sm">
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
const isAvailable = !!filterDays(index).length;
return (
<li key={day} className="my-6 flex border-transparent last:mb-2">
<span
className={classNames(
"w-20 font-medium sm:w-32 ",
!isAvailable ? "text-subtle line-through" : "text-default"
)}>
{day}
</span>
{isLoading ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
{filterDays(index).map((dayRange, i) => (
<div key={i} className="text-default flex items-center leading-4">
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ms-4">-</span>
<div className="ml-6 sm:w-28">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
</div>
) : (
<span className="text-subtle ml-6 sm:ml-0">{t("unavailable")}</span>
)}
</li>
);
})}
</ol>
<hr className="border-subtle" />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
{!!schedule?.id && !schedule.isManaged && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
rel="noopener noreferrer">
{t("edit_availability")}
</Button>
)}
</div>
</div>
</div>
);
};
);
}
);
EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();

View File

@ -54,7 +54,12 @@ const getEmbedNamespace = (query: ReturnType<typeof useRouter>["query"]) => {
return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
};
const CustomI18nextProvider = (props: AppPropsWithChildren) => {
// We dont need to pass nonce to the i18n provider - this was causing x2-x3 re-renders on a hard refresh
type AppPropsWithoutNonce = Omit<AppPropsWithChildren, "pageProps"> & {
pageProps: Omit<AppPropsWithChildren["pageProps"], "nonce">;
};
const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
/**
* i18n should never be clubbed with other queries, so that it's caching can be managed independently.
* We intend to not cache i18n query
@ -222,11 +227,19 @@ const AppProviders = (props: AppPropsWithChildren) => {
const session = trpc.viewer.public.session.useQuery().data;
// No need to have intercom on public pages - Good for Page Performance
const isPublicPage = usePublicPage();
const { pageProps, ...rest } = props;
const { _nonce, ...restPageProps } = pageProps;
const propsWithoutNonce = {
pageProps: {
...restPageProps,
},
...rest,
};
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={session || undefined}>
<CustomI18nextProvider {...props}>
<CustomI18nextProvider {...propsWithoutNonce}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider

View File

@ -1,3 +1,5 @@
import { memo } from "react";
import { FilterCheckboxFieldsContainer } from "@calcom/features/filters/components/TeamsFilter";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc";
@ -14,7 +16,7 @@ const mapEventTypeToOption = (eventType: EventType): Option => ({
label: eventType.teamId ? `${eventType.title} (${eventType.team?.name})` : eventType.title,
});
export const EventTypeList = () => {
export const EventTypeList = memo(() => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedTeamId, selectedEventTypeId, selectedUserId, isAll } = filter;
@ -72,4 +74,6 @@ export const EventTypeList = () => {
</FilterCheckboxFieldsContainer>
</AnimatedPopover>
);
};
});
EventTypeList.displayName = "EventTypeList";

View File

@ -5,7 +5,7 @@ import Link from "next/link";
import type { NextRouter } from "next/router";
import { useRouter } from "next/router";
import type { Dispatch, ReactNode, SetStateAction } from "react";
import React, { Fragment, useEffect, useState, useRef } from "react";
import React, { Fragment, useEffect, useState, useRef, useMemo } from "react";
import { Toaster } from "react-hot-toast";
import dayjs from "@calcom/dayjs";
@ -801,15 +801,18 @@ const getOrganizationUrl = (slug: string) =>
function SideBar({ bannersHeight, user }: SideBarProps) {
const { t, isLocaleReady } = useLocale();
const router = useRouter();
const orgBranding = useOrgBrandingValues();
const publicPageUrl = orgBranding?.slug ? getOrganizationUrl(orgBranding?.slug) : "";
const publicPageUrl = useMemo(() => {
if (!user?.organizationId) return `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`;
const publicPageUrl = orgBranding?.slug ? getOrganizationUrl(orgBranding?.slug) : "";
return publicPageUrl;
}, [orgBranding?.slug, user?.organizationId, user?.username]);
const bottomNavItems: NavigationItemType[] = [
{
name: "view_public_page",
href: !!user?.organizationId
? publicPageUrl
: `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`,
href: publicPageUrl,
icon: ExternalLink,
target: "__blank",
},
@ -818,9 +821,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
href: "",
onClick: (e: { preventDefault: () => void }) => {
e.preventDefault();
navigator.clipboard.writeText(
!!user?.organizationId ? publicPageUrl : `${process.env.NEXT_PUBLIC_WEBSITE_URL}/${user?.username}`
);
navigator.clipboard.writeText(publicPageUrl);
showToast(t("link_copied"), "success");
},
icon: Copy,
@ -898,6 +899,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
{bottomNavItems.map(({ icon: Icon, ...item }, index) => (
<Tooltip side="right" content={t(item.name)} className="lg:hidden" key={item.name}>
<ButtonOrLink
id={item.name}
href={item.href || undefined}
aria-label={t(item.name)}
target={item.target}
@ -908,11 +910,6 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
isLocaleReady ? "hover:bg-emphasis hover:text-emphasis" : "",
index === 0 && "mt-3"
)}
aria-current={
defaultIsCurrent && defaultIsCurrent({ item: { href: item.href }, router })
? "page"
: undefined
}
onClick={item.onClick}>
{!!Icon && (
<Icon
@ -921,11 +918,6 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
"me-3 md:ltr:mr-2 md:rtl:ml-2"
)}
aria-hidden="true"
aria-current={
defaultIsCurrent && defaultIsCurrent({ item: { href: item.href }, router })
? "page"
: undefined
}
/>
)}
{isLocaleReady ? (

View File

@ -1,5 +1,4 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useState } from "react";
import { useState, memo } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
@ -98,9 +97,7 @@ export const tips = [
const reversedTips = tips.slice(0).reverse();
export default function Tips() {
const [animationRef] = useAutoAnimate<HTMLDivElement>();
function Tips() {
const { t } = useLocale();
const [list, setList] = useState<typeof tips>(() => {
@ -175,3 +172,5 @@ export default function Tips() {
</div>
);
}
export default memo(Tips);

View File

@ -0,0 +1,21 @@
import { useRef, useEffect } from "react";
export function useTraceUpdate(props: { [s: string]: unknown } | ArrayLike<unknown>) {
const prev = useRef(props);
useEffect(() => {
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: fix this
if (prev.current[k] !== v) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore TODO: fix this
ps[k] = [prev.current[k], v];
}
return ps;
}, {});
if (Object.keys(changedProps).length > 0) {
console.log("Changed props:", changedProps);
}
prev.current = props;
});
}