Skip to content

Commit

Permalink
⚡️ Prefetch queries with trpc (#1454)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukevella authored Dec 2, 2024
1 parent 40df1ff commit 82ebcd8
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 129 deletions.
45 changes: 26 additions & 19 deletions apps/web/src/app/[locale]/(admin)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,45 @@
import { cn } from "@rallly/ui";
import { dehydrate, Hydrate } from "@tanstack/react-query";
import React from "react";

import { MobileNavigation } from "@/app/[locale]/(admin)/mobile-navigation";
import { ProBadge } from "@/app/[locale]/(admin)/pro-badge";
import { Sidebar } from "@/app/[locale]/(admin)/sidebar";
import { LogoLink } from "@/app/components/logo-link";
import { PayWallDialog } from "@/components/pay-wall-dialog";
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";

export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const helpers = await createSSRHelper();
await helpers.user.subscription.prefetch();
const dehydratedState = dehydrate(helpers.queryClient);
return (
<PayWallDialog>
<div className="flex flex-col pb-16 md:pb-0">
<div
className={cn(
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-4 overflow-y-auto p-6 md:flex",
)}
>
<div className="flex w-full items-center justify-between gap-4">
<LogoLink />
<ProBadge />
<Hydrate state={dehydratedState}>
<PayWallDialog>
<div className="flex flex-col pb-16 md:pb-0">
<div
className={cn(
"fixed inset-y-0 z-50 hidden w-72 shrink-0 flex-col gap-y-4 overflow-y-auto p-6 md:flex",
)}
>
<div className="flex w-full items-center justify-between gap-4">
<LogoLink />
<ProBadge />
</div>
<Sidebar />
</div>
<div className={cn("grow space-y-4 p-3 md:ml-72 md:p-4 lg:p-6")}>
<div className="max-w-5xl">{children}</div>
</div>
<div className="fixed bottom-0 z-20 flex h-16 w-full flex-col justify-center bg-gray-100/90 backdrop-blur-md md:hidden">
<MobileNavigation />
</div>
<Sidebar />
</div>
<div className={cn("grow space-y-4 p-3 md:ml-72 md:p-4 lg:p-6")}>
<div className="max-w-5xl">{children}</div>
</div>
<div className="fixed bottom-0 z-20 flex h-16 w-full flex-col justify-center bg-gray-100/90 backdrop-blur-md md:hidden">
<MobileNavigation />
</div>
</div>
</PayWallDialog>
</PayWallDialog>
</Hydrate>
);
}
40 changes: 23 additions & 17 deletions apps/web/src/app/[locale]/(admin)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { dehydrate, Hydrate } from "@tanstack/react-query";
import { HomeIcon } from "lucide-react";
import { Trans } from "react-i18next/TransWithoutContext";

Expand All @@ -11,27 +12,32 @@ import {
PageTitle,
} from "@/app/components/page-layout";
import { getTranslation } from "@/i18n/server";
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";

export default async function Page({ params }: { params: Params }) {
const { t } = await getTranslation(params.locale);
const helpers = await createSSRHelper();
await helpers.dashboard.info.prefetch();
return (
<div>
<PageContainer>
<PageHeader>
<div className="flex items-center gap-x-3">
<PageIcon>
<HomeIcon />
</PageIcon>
<PageTitle>
<Trans t={t} i18nKey="home" defaults="Home" />
</PageTitle>
</div>
</PageHeader>
<PageContent>
<Dashboard />
</PageContent>
</PageContainer>
</div>
<Hydrate state={dehydrate(helpers.queryClient)}>
<div>
<PageContainer>
<PageHeader>
<div className="flex items-center gap-x-3">
<PageIcon>
<HomeIcon />
</PageIcon>
<PageTitle>
<Trans t={t} i18nKey="home" defaults="Home" />
</PageTitle>
</div>
</PageHeader>
<PageContent>
<Dashboard />
</PageContent>
</PageContainer>
</div>
</Hydrate>
);
}

Expand Down
79 changes: 23 additions & 56 deletions apps/web/src/app/[locale]/invite/[urlId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,26 @@
"use client";
import { notFound, useParams, useSearchParams } from "next/navigation";
import React from "react";

import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { VisibilityProvider } from "@/components/visibility";
import { PermissionsContext } from "@/contexts/permissions";
import { trpc } from "@/trpc/client";

import Loader from "./loading";

const Prefetch = ({ children }: React.PropsWithChildren) => {
const searchParams = useSearchParams();
const token = searchParams?.get("token") as string;
const params = useParams<{ urlId: string }>();
const urlId = params?.urlId as string;
const { data: permission } = trpc.auth.getUserPermission.useQuery(
{ token },
{
enabled: !!token,
},
);

const { data: poll, error } = trpc.polls.get.useQuery(
{ urlId },
{
retry: false,
},
);

const { data: participants } = trpc.polls.participants.list.useQuery({
pollId: urlId,
});

const comments = trpc.polls.comments.list.useQuery({ pollId: urlId });

if (error?.data?.code === "NOT_FOUND") {
notFound();
}
if (!poll || !participants || !comments.isFetched) {
return <Loader />;
}

return (
<PermissionsContext.Provider value={{ userId: permission?.userId ?? null }}>
{children}
</PermissionsContext.Provider>
);
};

export default function Layout({ children }: { children: React.ReactNode }) {
import { dehydrate, Hydrate } from "@tanstack/react-query";

import { createSSRHelper } from "@/trpc/server/create-ssr-helper";

import Providers from "./providers";

export default async function Layout({
children,
params,
}: {
params: { urlId: string };
children: React.ReactNode;
}) {
const trpc = await createSSRHelper();

await Promise.all([
trpc.polls.get.prefetch({ urlId: params.urlId }),
trpc.polls.participants.list.prefetch({ pollId: params.urlId }),
trpc.polls.comments.list.prefetch({ pollId: params.urlId }),
]);
return (
<Prefetch>
<LegacyPollContextProvider>
<VisibilityProvider>{children}</VisibilityProvider>
</LegacyPollContextProvider>
</Prefetch>
<Hydrate state={dehydrate(trpc.queryClient)}>
<Providers>{children}</Providers>
</Hydrate>
);
}
32 changes: 30 additions & 2 deletions apps/web/src/app/[locale]/invite/[urlId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,38 @@ import { absoluteUrl } from "@rallly/utils/absolute-url";
import { notFound } from "next/navigation";

import { InvitePage } from "@/app/[locale]/invite/[urlId]/invite-page";
import { PermissionProvider } from "@/contexts/permissions";
import { getTranslation } from "@/i18n/server";
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";

export default async function Page() {
return <InvitePage />;
const PermissionContext = async ({
children,
token,
}: React.PropsWithChildren<{ token?: string }>) => {
const helpers = await createSSRHelper();
let impersonatedUserId: string | null = null;
if (token) {
const res = await helpers.auth.getUserPermission.fetch({ token });
impersonatedUserId = res?.userId ?? null;
}
return (
<PermissionProvider userId={impersonatedUserId}>
{children}
</PermissionProvider>
);
};

export default async function Page({
searchParams,
}: {
params: { urlId: string };
searchParams: { token: string };
}) {
return (
<PermissionContext token={searchParams.token}>
<InvitePage />
</PermissionContext>
);
}

export async function generateMetadata({
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/app/[locale]/invite/[urlId]/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { VisibilityProvider } from "@/components/visibility";

export default function Providers({ children }: { children: React.ReactNode }) {
return (
<LegacyPollContextProvider>
<VisibilityProvider>{children}</VisibilityProvider>
</LegacyPollContextProvider>
);
}
18 changes: 17 additions & 1 deletion apps/web/src/app/[locale]/poll/[urlId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { prisma } from "@rallly/database";
import { dehydrate, Hydrate } from "@tanstack/react-query";
import { notFound } from "next/navigation";

import { PollLayout } from "@/components/layouts/poll-layout";
import { createSSRHelper } from "@/trpc/server/create-ssr-helper";

export default async function Layout({
children,
params,
}: React.PropsWithChildren<{ params: { urlId: string } }>) {
const trpc = await createSSRHelper();

// Prefetch all queries used in PollLayout
await Promise.all([
trpc.polls.get.prefetch({ urlId: params.urlId }),
trpc.polls.participants.list.prefetch({ pollId: params.urlId }),
trpc.polls.getWatchers.prefetch({ pollId: params.urlId }),
trpc.polls.comments.list.prefetch({ pollId: params.urlId }),
]);

const poll = await prisma.poll.findUnique({ where: { id: params.urlId } });
if (!poll) {
notFound();
}

return <PollLayout>{children}</PollLayout>;
return (
<Hydrate state={dehydrate(trpc.queryClient)}>
<PollLayout>{children}</PollLayout>
</Hydrate>
);
}

export async function generateMetadata({
Expand Down
12 changes: 3 additions & 9 deletions apps/web/src/components/participants-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@ export const ParticipantsProvider: React.FunctionComponent<{
children?: React.ReactNode;
pollId: string;
}> = ({ children, pollId }) => {
const { data: participants } = trpc.polls.participants.list.useQuery(
{
pollId,
},
{
staleTime: 1000 * 10,
cacheTime: Infinity,
},
);
const { data: participants } = trpc.polls.participants.list.useQuery({
pollId,
});

const getParticipants = (
optionId: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
"use client";
import React from "react";

import { useParticipants } from "@/components/participants-provider";
import { useUser } from "@/components/user-provider";
import { usePoll } from "@/contexts/poll";
import { useRole } from "@/contexts/role";

export const PermissionsContext = React.createContext<{
const PermissionsContext = React.createContext<{
userId: string | null;
}>({
userId: null,
});

export const PermissionProvider = ({
children,
userId,
}: React.PropsWithChildren<{ userId: string | null }>) => {
return (
<PermissionsContext.Provider value={{ userId }}>
{children}
</PermissionsContext.Provider>
);
};

export const usePermissions = () => {
const poll = usePoll();
const context = React.useContext(PermissionsContext);
Expand Down
11 changes: 1 addition & 10 deletions apps/web/src/contexts/poll.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import { useParams } from "next/navigation";
import React from "react";

import { trpc } from "@/trpc/client";

export const usePoll = () => {
const params = useParams<{ urlId: string }>();

const [urlId] = React.useState(params?.urlId as string);

const pollQuery = trpc.polls.get.useQuery(
{ urlId },
{
staleTime: Infinity,
},
);
const pollQuery = trpc.polls.get.useQuery({ urlId: params?.urlId as string });

if (!pollQuery.data) {
throw new Error("Expected poll to be prefetched");
Expand Down
11 changes: 5 additions & 6 deletions apps/web/src/pages/api/trpc/[trpc].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { posthogApiHandler } from "@rallly/posthog/server";
import * as Sentry from "@sentry/nextjs";
import { TRPCError } from "@trpc/server";
import { createNextApiHandler } from "@trpc/server/adapters/next";
import requestIp from "request-ip";

import { getServerSession } from "@/auth";
import type { TRPCContext } from "@/trpc/context";
import type { AppRouter } from "@/trpc/routers";
import { appRouter } from "@/trpc/routers";
import { getEmailClient } from "@/utils/emails";
Expand All @@ -27,19 +29,16 @@ const trpcApiHandler = createNextApiHandler<AppRouter>({
});
}

const res = {
return {
user: {
id: session.user.id,
isGuest: session.user.email === null,
locale: session.user.locale ?? undefined,
image: session.user.image ?? undefined,
getEmailClient: () => getEmailClient(session.user.locale ?? undefined),
},
req: opts.req,
res: opts.res,
};

return res;
ip: requestIp.getClientIp(opts.req) ?? undefined,
} satisfies TRPCContext;
},
onError({ error }) {
if (error.code === "INTERNAL_SERVER_ERROR") {
Expand Down
Loading

0 comments on commit 82ebcd8

Please sign in to comment.