perf: Avoid unmounting of Shell on navigation and thus reduce number of paints (#10646)

Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Hariom Balhara 2023-08-10 04:24:51 +05:30 committed by GitHub
parent 37ce8860b5
commit a49c34e733
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 221 additions and 158 deletions

View File

@ -3,7 +3,7 @@ import { useRouter } from "next/navigation";
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui";
import { AlertCircle } from "@calcom/ui/components/icon";
@ -12,7 +12,7 @@ type AppsLayoutProps = {
children: React.ReactNode;
actions?: (className?: string) => JSX.Element;
emptyStore?: boolean;
} & Omit<ComponentProps<typeof Shell>, "actions">;
} & Omit<ComponentProps<typeof ShellMain>, "actions">;
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
const { t } = useLocale();
@ -22,7 +22,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
if (session.status === "loading") return <></>;
return (
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<ShellMain {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<div className="flex flex-col xl:flex-row">
<main className="w-full">
{emptyStore ? (
@ -38,7 +38,6 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
)}
</main>
</div>
</Shell>
</ShellMain>
);
}
export const getLayout = (page: React.ReactElement) => <AppsLayout>{page}</AppsLayout>;

View File

@ -2,7 +2,7 @@ import { usePathname, useSearchParams } from "next/navigation";
export default function useIsBookingPage() {
const pathname = usePathname();
const isBookingPage = ["/booking", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const isBookingPage = ["/booking/", "/cancel", "/reschedule"].some((route) => pathname?.startsWith(route));
const searchParams = useSearchParams();
const userParam = searchParams.get("user");

View File

@ -1,3 +1,5 @@
import React from "react";
import { trpc } from "@calcom/trpc/react";
import type { AppProps } from "@lib/app-providers";

View File

@ -3,6 +3,7 @@ import type { ChangeEventHandler } from "react";
import { useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getLayout } from "@calcom/features/MainLayout";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
@ -94,6 +95,7 @@ export default function Apps({
}
Apps.PageWrapper = PageWrapper;
Apps.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;

View File

@ -1,8 +1,9 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter } from "next/navigation";
import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { RouterOutputs } from "@calcom/trpc/react";
@ -130,15 +131,17 @@ export default function AvailabilityPage() {
const { t } = useLocale();
return (
<div>
<Shell
<ShellMain
heading={t("availability")}
hideHeadingOnMobile
subtitle={t("configure_availability")}
CTA={<NewScheduleButton />}>
<WithQuery success={({ data }) => <AvailabilityList {...data} />} customLoader={<SkeletonLoader />} />
</Shell>
</ShellMain>
</div>
);
}
AvailabilityPage.getLayout = getLayout;
AvailabilityPage.PageWrapper = PageWrapper;

View File

@ -1,16 +1,21 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { GetStaticPaths, GetStaticProps } from "next";
import { Fragment } from "react";
import React from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
import { getLayout } from "@calcom/features/MainLayout";
import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { ShellMain } from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { HorizontalTabs } from "@calcom/ui";
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import { Alert, Button, EmptyScreen } from "@calcom/ui";
import { Calendar } from "@calcom/ui/components/icon";
@ -32,6 +37,28 @@ type RecurringInfo = {
bookings: { [key: string]: Date[] };
};
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
const validStatuses = ["upcoming", "recurring", "past", "cancelled", "unconfirmed"] as const;
const descriptionByStatus: Record<NonNullable<BookingListingStatus>, string> = {
@ -112,90 +139,101 @@ export default function Bookings() {
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
return (
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
<ShellMain hideHeadingOnMobile heading={t("bookings")} subtitle={t("bookings_description")}>
<div className="flex flex-col">
<div className="flex flex-col flex-wrap lg:flex-row">
<HorizontalTabs tabs={tabs} />
<div className="max-w-full overflow-x-auto xl:ml-auto">
<FiltersContainer />
</div>
</div>
<main className="w-full">
<div className="flex w-full flex-col" ref={animationParentRef}>
{query.status === "error" && (
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
)}
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
{query.status === "success" && !isEmpty && (
<>
{!!bookingsToday.length && status === "upcoming" && (
<div className="mb-6 pt-2 xl:pt-0">
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<p className="text-subtle mb-2 text-xs font-medium uppercase leading-4">{t("today")}</p>
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="today-bookings">
<Fragment>
{bookingsToday.map((booking: BookingOutput) => (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfoToday}
{...booking}
/>
))}
</Fragment>
</tbody>
</table>
</div>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</Fragment>
</tbody>
</table>
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
const recurringInfo = page.recurringInfo.find(
(info) => info.recurringEventId === booking.recurringEventId
);
return (
<BookingListItem
key={booking.id}
listingStatus={status}
recurringInfo={recurringInfo}
{...booking}
/>
);
})}
</Fragment>
))}
</tbody>
</table>
</div>
<div className="text-default p-4 text-center" ref={buttonInView.ref}>
<Button
color="minimal"
loading={query.isFetchingNextPage}
disabled={!query.hasNextPage}
onClick={() => query.fetchNextPage()}>
{query.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Calendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
)}
</main>
</div>
</BookingLayout>
</ShellMain>
);
}
Bookings.PageWrapper = PageWrapper;
Bookings.getLayout = getLayout;
export const getStaticProps: GetStaticProps = async (ctx) => {
const params = querySchema.safeParse(ctx.params);

View File

@ -7,6 +7,7 @@ import type { FC } from "react";
import { memo, useEffect, useState } from "react";
import { z } from "zod";
import { getLayout } from "@calcom/features/MainLayout";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
@ -15,7 +16,7 @@ import CreateEventTypeDialog from "@calcom/features/eventtypes/components/Create
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -932,25 +933,25 @@ const EventTypesPage = () => {
}, []);
return (
<div>
<ShellMain
withoutSeo
heading={t("event_types_page_title")}
hideHeadingOnMobile
subtitle={t("event_types_page_subtitle")}
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA data={data} />}>
<HeadSeo
title="Event Types"
description="Create events to share for people to book on your calendar."
/>
<Shell
withoutSeo
heading={t("event_types_page_title")}
hideHeadingOnMobile
subtitle={t("event_types_page_subtitle")}
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA data={data} />}>
<Main data={data} status={status} error={error} filters={filters} />
</Shell>
</div>
<Main data={data} status={status} error={error} filters={filters} />
</ShellMain>
);
};
EventTypesPage.getLayout = getLayout;
EventTypesPage.PageWrapper = PageWrapper;
export default EventTypesPage;

View File

@ -1,3 +1,4 @@
import { getLayout } from "@calcom/features/MainLayout";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import {
AverageEventDurationChart,
@ -9,7 +10,7 @@ import {
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -56,7 +57,7 @@ export default function InsightsPage() {
return (
<div>
<Shell hideHeadingOnMobile>
<ShellMain hideHeadingOnMobile>
<UpgradeTip
title={t("make_informed_decisions")}
description={t("make_informed_decisions_description")}
@ -111,12 +112,13 @@ export default function InsightsPage() {
</FiltersProvider>
)}
</UpgradeTip>
</Shell>
</ShellMain>
</div>
);
}
InsightsPage.PageWrapper = PageWrapper;
InsightsPage.getLayout = getLayout;
// If feature flag is disabled, return not found on getServerSideProps
export const getServerSideProps = async () => {

View File

@ -1,7 +1,8 @@
import type { GetServerSidePropsContext } from "next";
import { getLayout } from "@calcom/features/MainLayout";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { ShellMain } from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@ -17,7 +18,7 @@ function Teams() {
const [user] = trpc.viewer.me.useSuspenseQuery();
return (
<Shell
<ShellMain
heading={t("teams")}
hideHeadingOnMobile
subtitle={t("create_manage_teams_collaborative")}
@ -33,7 +34,7 @@ function Teams() {
)
}>
<TeamsListing />
</Shell>
</ShellMain>
);
}
@ -46,5 +47,5 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
Teams.requiresLicense = false;
Teams.PageWrapper = PageWrapper;
Teams.getLayout = getLayout;
export default Teams;

View File

@ -0,0 +1,17 @@
import { useRef } from "react";
/**
* Updates in document the number of times a component has been rendered. Helps in 2 ways. Using it doesn't cause any additional renders.
* - Did the component render when it shouldn't have?
* - Did the component reset its state when it shouldn't have?
*/
export const RenderCounter = ({ label }: { label: string }) => {
const counterRef = useRef(0);
counterRef.current++;
return (
<span>
<span>{label}:</span>
<span>{counterRef.current} </span>
</span>
);
};

View File

@ -0,0 +1 @@
export { RenderCounter } from "./components/RenderCounter";

View File

@ -0,0 +1,7 @@
{
"name": "@calcom/debugging",
"description": "Debugging utilities",
"private": true,
"version": "1.0.0",
"main": "./index.ts"
}

View File

@ -0,0 +1,15 @@
{
"extends": "@calcom/tsconfig/react-library.json",
"compilerOptions": {
"resolveJsonModule": true
},
"include": [
"../../apps/web/next-env.d.ts",
"../types/*.d.ts",
"../types/next-auth.d.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -0,0 +1,17 @@
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
export default function MainLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell withoutMain={true} {...rest}>
{children}
</Shell>
);
}
export const getLayout = (page: React.ReactElement) => <MainLayout>{page}</MainLayout>;

View File

@ -1,51 +0,0 @@
import type { ComponentProps } from "react";
import React from "react";
import Shell from "@calcom/features/shell/Shell";
import { HorizontalTabs } from "@calcom/ui";
import type { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui";
import { FiltersContainer } from "../components/FiltersContainer";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
name: "upcoming",
href: "/bookings/upcoming",
},
{
name: "unconfirmed",
href: "/bookings/unconfirmed",
},
{
name: "recurring",
href: "/bookings/recurring",
},
{
name: "past",
href: "/bookings/past",
},
{
name: "cancelled",
href: "/bookings/cancelled",
},
];
export default function BookingLayout({
children,
...rest
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
return (
<Shell {...rest} hideHeadingOnMobile>
<div className="flex flex-col">
<div className="flex flex-col flex-wrap lg:flex-row">
<HorizontalTabs tabs={tabs} />
<div className="max-w-full overflow-x-auto xl:ml-auto">
<FiltersContainer />
</div>
</div>
<main className="w-full">{children}</main>
</div>
</Shell>
);
}
export const getLayout = (page: React.ReactElement) => <BookingLayout>{page}</BookingLayout>;

View File

@ -3,7 +3,8 @@ import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import Shell from "@calcom/features/shell/Shell";
import { getLayout } from "@calcom/features/MainLayout";
import { ShellMain } from "@calcom/features/shell/Shell";
import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -49,7 +50,7 @@ function WorkflowsPage() {
});
return (
<Shell
<ShellMain
heading={t("workflows")}
title={t("workflows")}
subtitle={t("workflows_to_automate_notifications")}
@ -92,7 +93,7 @@ function WorkflowsPage() {
</FilterResults>
</>
</LicenseRequired>
</Shell>
</ShellMain>
);
}
@ -221,4 +222,6 @@ const Filter = (props: {
);
};
WorkflowsPage.getLayout = getLayout;
export default WorkflowsPage;

View File

@ -4026,6 +4026,12 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/debugging@workspace:packages/debugging":
version: 0.0.0-use.local
resolution: "@calcom/debugging@workspace:packages/debugging"
languageName: unknown
linkType: soft
"@calcom/discord@workspace:packages/app-store/discord":
version: 0.0.0-use.local
resolution: "@calcom/discord@workspace:packages/app-store/discord"