perf: Remove ssrInit from layout and event-types (#20368)

* perf: Remove ssrInit from layout and event-types

* remove ssg file since not used

* refator trpc usage in app router

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
This commit is contained in:
Keith Williams
2025-03-26 22:04:52 -03:00
committed by GitHub
co-authored by hbjORbj
parent 2ab4e8b40a
commit be883aac02
14 changed files with 130 additions and 240 deletions
@@ -8,8 +8,6 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
import EventTypes, { EventTypesCTA } from "~/event-types/views/event-types-listing-view";
export const generateMetadata = async () =>
@@ -26,7 +24,6 @@ const Page = async ({ params, searchParams }: PageProps) => {
redirect("/auth/login");
}
await ssrInit(context);
const t = await getTranslate();
return (
@@ -4,8 +4,6 @@ import { cookies, headers } from "next/headers";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
import Logout from "~/auth/logout-view";
export const generateMetadata = async () => {
@@ -19,7 +17,6 @@ const Page = async ({ params, searchParams }: PageProps) => {
// cookie will be cleared in `/apps/web/middleware.ts`
const h = await headers();
const context = buildLegacyCtx(h, await cookies(), await params, await searchParams);
await ssrInit(context);
return <Logout query={context.query} />;
};
-8
View File
@@ -1,8 +0,0 @@
"use client";
import { createHydrateClient } from "app/_trpc/createHydrateClient";
import superjson from "superjson";
export const HydrateClient = createHydrateClient({
transformer: superjson,
});
@@ -1,26 +0,0 @@
"use client";
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
import { useMemo } from "react";
type DataTransformer = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
serialize: (data: any) => any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
deserialize: (data: any) => any;
};
export function createHydrateClient(opts: { transformer: DataTransformer }) {
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
const { state, children } = props;
const transformedState: DehydratedState = useMemo(() => {
if (opts.transformer) {
return opts.transformer.deserialize(state);
}
return state;
}, [state]);
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
};
}
+37
View File
@@ -0,0 +1,37 @@
"use client";
import { QueryClient } from "@tanstack/react-query";
import { TRPCClientError } from "@trpc/client";
const MAX_QUERY_RETRIES = 3;
const isTRPCClientError = (cause: unknown): cause is TRPCClientError<any> => {
return cause instanceof TRPCClientError;
};
export const queryClient = new QueryClient({
// these configurations are copied from "packages/trpc/react/trpc.ts"
defaultOptions: {
queries: {
/**
* 1s should be enough to just keep identical query waterfalls low
* @example if one page components uses a query that is also used further down the tree
*/
staleTime: 1000,
/**
* Retry `useQuery()` calls depending on this function
*/
retry(failureCount, error) {
if (isTRPCClientError(error) && error.data) {
const { code } = error.data;
if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") {
// if input data is wrong or you're not authorized there's no point retrying a query
return false;
}
}
return failureCount < MAX_QUERY_RETRIES;
},
},
},
});
-4
View File
@@ -1,4 +0,0 @@
import type { TRPCContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
export const getServerCaller = (ctx: TRPCContext) => appRouter.createCaller(ctx);
+80
View File
@@ -0,0 +1,80 @@
"use client";
import superjson from "superjson";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
import { httpBatchLink, httpLink, loggerLink, splitLink } from "@trpc/client";
import { trpc } from "./trpc";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveEndpoint = (links: any) => {
// TODO: Update our trpc routes so they are more clear.
// This function parses paths like the following and maps them
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
let endpoint;
let path = "";
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
};
};
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
export const trpcClient = trpc.createClient({
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
(typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === "number" &&
process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) ||
(opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
condition: (op) => !!op.context.skipBatch,
// when condition is true, use normal request
true: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
// when condition is false, use batch request
false: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpBatchLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
}),
],
transformer: superjson,
});
+6 -124
View File
@@ -1,137 +1,19 @@
"use client";
import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { HydrateClient } from "app/_trpc/HydrateClient";
import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { QueryClientProvider } from "@tanstack/react-query";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
import { httpBatchLink, httpLink, loggerLink, splitLink, TRPCClientError } from "@trpc/client";
export type Endpoint = (typeof ENDPOINTS)[number];
const MAX_QUERY_RETRIES = 3;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolveEndpoint = (links: any) => {
// TODO: Update our trpc routes so they are more clear.
// This function parses paths like the following and maps them
// to the correct API endpoints.
// - viewer.me - 2 segment paths like this are for logged in requests
// - viewer.public.i18n - 3 segments paths can be public or authed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ctx: any) => {
const parts = ctx.op.path.split(".");
let endpoint;
let path = "";
if (parts.length == 2) {
endpoint = parts[0] as keyof typeof links;
path = parts[1];
} else {
endpoint = parts[1] as keyof typeof links;
path = parts.splice(2, parts.length - 2).join(".");
}
return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
};
};
const isTRPCClientError = (cause: unknown): cause is TRPCClientError<AppRouter> => {
return cause instanceof TRPCClientError;
};
import { queryClient } from "./query-client";
import { trpc } from "./trpc";
import { trpcClient } from "./trpc-client";
type Props = {
children: React.ReactNode;
dehydratedState: DehydratedState;
};
export const TrpcProvider = ({ children, dehydratedState }: Props) => {
const [queryClient] = useState(
() =>
new QueryClient({
// these configurations are copied from "packages/trpc/react/trpc.ts"
defaultOptions: {
queries: {
/**
* 1s should be enough to just keep identical query waterfalls low
* @example if one page components uses a query that is also used further down the tree
*/
staleTime: 1000,
/**
* Retry `useQuery()` calls depending on this function
*/
retry(failureCount, error) {
if (isTRPCClientError(error) && error.data) {
const { code } = error.data;
if (code === "BAD_REQUEST" || code === "FORBIDDEN" || code === "UNAUTHORIZED") {
// if input data is wrong or you're not authorized there's no point retrying a query
return false;
}
}
return failureCount < MAX_QUERY_RETRIES;
},
},
},
})
);
const url =
typeof window !== "undefined"
? "/api/trpc"
: process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}/api/trpc`
: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
// adds pretty logs to your console in development and logs errors in production
loggerLink({
enabled: (opts) =>
(typeof process.env.NEXT_PUBLIC_LOGGER_LEVEL === "number" &&
process.env.NEXT_PUBLIC_LOGGER_LEVEL >= 0) ||
(opts.direction === "down" && opts.result instanceof Error),
}),
splitLink({
// check for context property `skipBatch`
condition: (op) => !!op.context.skipBatch,
// when condition is true, use normal request
true: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
// when condition is false, use batch request
false: (runtime) => {
const links = Object.fromEntries(
ENDPOINTS.map((endpoint) => [
endpoint,
httpBatchLink({
url: `${url}/${endpoint}`,
})(runtime),
])
);
return resolveEndpoint(links);
},
}),
],
transformer: superjson,
})
);
export const TrpcProvider = ({ children }: Props) => {
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<HydrateClient state={dehydratedState}>{children}</HydrateClient>
</QueryClientProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
};
@@ -1,3 +1,5 @@
"use client";
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
import { createTRPCReact } from "@trpc/react-query";
+1 -6
View File
@@ -8,10 +8,6 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { IconSprites } from "@calcom/ui/components/icon";
import { NotificationSoundHandler } from "@calcom/web/components/notification-sound-handler";
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
import { ssrInit } from "@server/lib/ssr";
import "../styles/globals.css";
import { SpeculationRules } from "./SpeculationRules";
import { Providers } from "./providers";
@@ -112,7 +108,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
? getFallbackProps()
: await getInitialProps(fullUrl);
const ssr = await ssrInit(buildLegacyCtx(h, await cookies(), {}, {}));
return (
<html
lang={locale}
@@ -178,7 +173,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
"/insights",
]}
/>
<Providers dehydratedState={ssr.dehydrate()}>{children}</Providers>
<Providers>{children}</Providers>
{!isEmbed && <NotificationSoundHandler />}
<NotificationSoundHandler />
</body>
+2 -4
View File
@@ -1,6 +1,5 @@
"use client";
import { type DehydratedState } from "@tanstack/react-query";
import { TrpcProvider } from "app/_trpc/trpc-provider";
import { SessionProvider } from "next-auth/react";
import CacheProvider from "react-inlinesvg/provider";
@@ -12,14 +11,13 @@ import PlainChat from "@lib/plain/dynamicProvider";
type ProvidersProps = {
children: React.ReactNode;
dehydratedState: DehydratedState;
};
export function Providers({ children, dehydratedState }: ProvidersProps) {
export function Providers({ children }: ProvidersProps) {
const isBookingPage = useIsBookingPage();
return (
<SessionProvider>
<TrpcProvider dehydratedState={dehydratedState}>
<TrpcProvider>
{!isBookingPage ? <PlainChat /> : null}
{/* @ts-expect-error FIXME remove this comment when upgrading typescript to v5 */}
<CacheProvider>
@@ -3,13 +3,9 @@ import type { GetServerSidePropsContext } from "next";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { ssrInit } from "@server/lib/ssr";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req } = context;
const ssr = await ssrInit(context);
const session = await getServerSession({ req });
let appStore;
@@ -29,7 +25,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
return {
props: {
categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
trpcState: ssr.dehydrate(),
trpcState: undefined,
},
};
};
+1 -5
View File
@@ -5,13 +5,9 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { UserRepository } from "@calcom/lib/server/repository/user";
import type { AppCategories } from "@calcom/prisma/enums";
import { ssrInit } from "@server/lib/ssr";
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req } = context;
const ssr = await ssrInit(context);
const session = await getServerSession({ req });
let appStore, userAdminTeamsIds: number[];
@@ -46,7 +42,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
}),
appStore,
userAdminTeams: userAdminTeamsIds,
trpcState: ssr.dehydrate(),
trpcState: undefined,
},
};
};
-52
View File
@@ -1,52 +0,0 @@
import type { GetStaticPropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import superjson from "superjson";
import { CALCOM_VERSION } from "@calcom/lib/constants";
import prisma, { readonlyPrisma } from "@calcom/prisma";
import { appRouter } from "@calcom/trpc/server/routers/_app";
import { createServerSideHelpers } from "@trpc/react-query/server";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { i18n } = require("@calcom/config/next-i18next.config");
/**
* Initialize static site rendering tRPC helpers.
* Provides a method to prefetch tRPC-queries in a `getStaticProps`-function.
* Automatically prefetches i18n based on the passed in `context`-object to prevent i18n-flickering.
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
*/
export async function ssgInit<TParams extends { locale?: string }>(opts: GetStaticPropsContext<TParams>) {
const requestedLocale = opts.params?.locale || opts.locale || i18n.defaultLocale;
const isSupportedLocale = i18n.locales.includes(requestedLocale);
if (!isSupportedLocale) {
console.warn(`Requested unsupported locale "${requestedLocale}"`);
}
const locale = isSupportedLocale ? requestedLocale : i18n.defaultLocale;
const _i18n = await serverSideTranslations(locale, ["common"]);
const ssg = createServerSideHelpers({
router: appRouter,
transformer: superjson,
ctx: {
prisma,
insightsDb: readonlyPrisma,
session: null,
locale,
i18n: _i18n,
},
});
// i18n translations are already retrieved from serverSideTranslations call, there is no need to run a i18n.fetch
// we can set query data directly to the queryClient
const queryKey = [
["viewer", "public", "i18n"],
{ input: { locale, CalComVersion: CALCOM_VERSION }, type: "query" },
];
ssg.queryClient.setQueryData(queryKey, { i18n: _i18n });
return ssg;
}