Skip to content

Commit

Permalink
105 improve the dx around the ssr stuff (#107)
Browse files Browse the repository at this point in the history
* Rework SSR to build up a data object on server
This simplifies the calls if you use SsrContextServer

* Add params to query key
Ensure that params are sorted before stringing
  • Loading branch information
byronwall committed Sep 30, 2023
1 parent 40ec15a commit 00345e8
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 64 deletions.
4 changes: 3 additions & 1 deletion src/app/SsrContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export type SsrQueryShape<TRouter extends AnyRouter> = {
? TRouterOrProcedure extends AnyRouter
? Partial<SsrQueryShape<TRouterOrProcedure>>
: TRouterOrProcedure extends AnyProcedure
? Partial<inferTransformedProcedureOutput<TRouterOrProcedure>>
? TRouterOrProcedure extends string // this is the JSONed params
? Partial<inferTransformedProcedureOutput<TRouterOrProcedure>>
: never
: never
: undefined;
};
Expand Down
16 changes: 16 additions & 0 deletions src/app/SsrContextServer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { getInitialData } from "~/hooks/useQuerySsrServer";

import { SsrContextProvider } from "./SsrContext";

export function SsrContextServer({ children }: { children: React.ReactNode }) {
// this handles some magic
// the callQuerySsrServer funcs are building up the initial data
// it's all happening in the object held by getInitialData()
const initialData = getInitialData();

return (
<SsrContextProvider initialData={initialData}>
{children}
</SsrContextProvider>
);
}
23 changes: 6 additions & 17 deletions src/app/awards/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import { SsrContextProvider } from "../SsrContext";
import { deepMerge } from "../deepMerge";
import { SsrContextServer } from "../SsrContextServer";

export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const initialData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardsForProfile
);
await callQuerySsrServer(appRouter.awardRouter.getAllAwardsForProfile);
await callQuerySsrServer(appRouter.awardRouter.getAllAwardImages, {
shouldLimitToProfile: true,
});

const moreData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardImages,
{
shouldLimitToProfile: true,
}
);

const mergedData = deepMerge(initialData, moreData);

return (
<SsrContextProvider initialData={mergedData}>{children}</SsrContextProvider>
);
return <SsrContextServer>{children}</SsrContextServer>;
}
10 changes: 4 additions & 6 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Provider from "./_trpc/Provider";
import { NextAuthProvider } from "./authProvider";
import { GlobalNotifications } from "./GlobalNotifications";
import { SentenceCreatorDialog } from "./SentenceCreatorDialog";
import { SsrContextProvider } from "./SsrContext";
import { SsrContextServer } from "./SsrContextServer";

import { UserMenuOrLogin } from "../components/UserMenuOrLogin";

Expand All @@ -35,16 +35,14 @@ export default async function RootLayout({
}) {
const session = await getServerAuthSession();

const initialData = await callQuerySsrServer(
appRouter.awardRouter.getAllAwardsForProfile
);
await callQuerySsrServer(appRouter.awardRouter.getAllAwardsForProfile);

return (
<html>
<body>
<NextAuthProvider session={session}>
<Provider>
<SsrContextProvider initialData={initialData}>
<SsrContextServer>
<div className="flex min-h-screen flex-col pb-20">
<header className="bg-background container z-40">
<div className="flex h-20 items-center justify-between py-6">
Expand All @@ -61,7 +59,7 @@ export default async function RootLayout({
<SentenceCreatorDialog />
</div>
<ReactQueryDevtools initialIsOpen={false} />
</SsrContextProvider>
</SsrContextServer>
</Provider>
</NextAuthProvider>
<Analytics />
Expand Down
10 changes: 4 additions & 6 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { QuestionPractice } from "~/components/QuestionPractice";
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import { SsrContextProvider } from "./SsrContext";
import { SsrContextServer } from "./SsrContextServer";

export default async function Home() {
const session = await getServerAuthSession();

const initialData = await callQuerySsrServer(
appRouter.questionRouter.getPossibleSentences
);
await callQuerySsrServer(appRouter.questionRouter.getPossibleSentences);

if (!session) {
return (
Expand All @@ -21,10 +19,10 @@ export default async function Home() {
}

return (
<SsrContextProvider initialData={initialData}>
<SsrContextServer>
<section className="flex flex-col items-center gap-4">
<QuestionPractice />
</section>
</SsrContextProvider>
</SsrContextServer>
);
}
15 changes: 6 additions & 9 deletions src/app/plan/[planNameSlugged]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { deslugify } from "~/utils";
import { SsrContextProvider } from "~/app/SsrContext";
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";
import { SsrContextServer } from "~/app/SsrContextServer";

import { LearningPlanSingle } from "./LessonPlanSingle";

Expand All @@ -16,16 +16,13 @@ export default async function Page({ params }: PageProps) {

const planName = deslugify(planNameSlugged);

const initialData = await callQuerySsrServer(
appRouter.planRouter.getSingleLearningPlan,
{
learningPlanName: planName,
}
);
await callQuerySsrServer(appRouter.planRouter.getSingleLearningPlan, {
learningPlanName: planName,
});

return (
<SsrContextProvider initialData={initialData}>
<SsrContextServer>
<LearningPlanSingle planName={planName} />
</SsrContextProvider>
</SsrContextServer>
);
}
12 changes: 3 additions & 9 deletions src/app/plan/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { callQuerySsrServer } from "~/hooks/useQuerySsrServer";
import { appRouter } from "~/server/api/root";

import { SsrContextProvider } from "../SsrContext";
import { SsrContextServer } from "../SsrContextServer";

export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const initialData = await callQuerySsrServer(
appRouter.planRouter.getAllLearningPlans
);
await callQuerySsrServer(appRouter.planRouter.getAllLearningPlans);

return (
<SsrContextProvider initialData={initialData}>
{children}
</SsrContextProvider>
);
return <SsrContextServer>{children}</SsrContextServer>;
}
10 changes: 4 additions & 6 deletions src/app/stats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ import { appRouter } from "~/server/api/root";

import { StatsDetail } from "./StatsDetail";

import { SsrContextProvider } from "../SsrContext";
import { SsrContextServer } from "../SsrContextServer";

export default async function StatsPage() {
// create sections for the results history and summary table

const initialData = await callQuerySsrServer(
appRouter.questionRouter.getUserStats
);
await callQuerySsrServer(appRouter.questionRouter.getUserStats);

return (
<SsrContextProvider initialData={initialData}>
<SsrContextServer>
<StatsDetail />
</SsrContextProvider>
</SsrContextServer>
);
}
15 changes: 15 additions & 0 deletions src/hooks/deepSortObjectByKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function deepSortObjectByKeys(obj: any) {
if (typeof obj !== "object") {
return obj;
}

const sortedObj = {};

Object.keys(obj)
.sort()
.forEach((key) => {
(sortedObj as any)[key] = deepSortObjectByKeys(obj[key]);
});

return sortedObj;
}
13 changes: 11 additions & 2 deletions src/hooks/useQuerySsr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { useContext } from "react";

import { SsrContext } from "~/app/SsrContext";

import { deepSortObjectByKeys } from "./deepSortObjectByKeys";

export function useQuerySsr<
QueryProcedure extends AnyQueryProcedure,
U,
Expand All @@ -29,15 +31,22 @@ export function useQuerySsr<
// @ts-expect-error - we don't expose _def on the type layer
const keys = proc._def().path as string[]; // will be ['awardRouter', 'getActiveProfile']

const paramsAsString = params
? JSON.stringify(deepSortObjectByKeys(params))
: "";

const fullQueryKey = keys.concat(paramsAsString);
// TODO: need to link the initial data to the the params also

// traverse the keys into the context object, assume arbitrary depth
const initialDataForProc = keys.reduce((acc, key) => {
const initialDataForProc = fullQueryKey.reduce((acc, key) => {
const possibleData = (acc as any)[key];
if (possibleData === undefined) {
// throw error if dev
if (process.env.NODE_ENV === "development") {
throw new Error(`Could not find initialData for ${keys.join(".")}`);
throw new Error(
`Could not find initialData for ${fullQueryKey.join(".")}`
);
}
return undefined;
}
Expand Down
41 changes: 33 additions & 8 deletions src/hooks/useQuerySsrServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,46 @@ import {
type ProcedureRouterRecord,
type ProcedureRecord,
} from "@trpc/server";
import { cache } from "react";

import { type SsrQueryShape } from "~/app/SsrContext";
import { getTrpcServer } from "~/app/_trpc/serverClient";
import { deepMerge } from "~/app/deepMerge";
import { appRouter } from "~/server/api/root";

import { deepSortObjectByKeys } from "./deepSortObjectByKeys";

const routerKeyMap = createRouterMap(appRouter);

type SsrShape = Partial<SsrQueryShape<typeof appRouter>>;

export const getInitialData = cache(() => {
const initialData: SsrShape = {};

return initialData;
});

export async function callQuerySsrServer<
QueryProcedure extends AnyQueryProcedure
>(
proc: QueryProcedure,
params?: inferProcedureInput<QueryProcedure>
): Promise<SsrQueryShape<typeof appRouter>> {
// this mess plays a game to use the true appRouter to get the key structure
// with that key structure, it then traverses the trpcServer to get the true
// caller for the procedure
): Promise<SsrShape> {
const newInitialData = await getDataForSingleProc(proc, params);

const initialData = getInitialData();

const mergedData = deepMerge(initialData, newInitialData);

// Object.assign to forcefully update the cache
// return the complete data object if someone wants it server side
return Object.assign(initialData, mergedData);
}

async function getDataForSingleProc<QueryProcedure extends AnyQueryProcedure>(
proc: QueryProcedure,
params?: inferProcedureInput<QueryProcedure>
) {
const trpcServer = await getTrpcServer();

const keys = routerKeyMap.get(proc);
Expand All @@ -31,6 +54,10 @@ export async function callQuerySsrServer<
throw new Error(`Could not find keys for procedure.`);
}

const paramsAsString = params
? JSON.stringify(deepSortObjectByKeys(params))
: "";

// get the true caller -- this needs to go through the createCaller proxy
// this all ensures that the context is setup correctly
const trpcServerProc = keys.reduce((acc, key) => {
Expand All @@ -41,19 +68,17 @@ export async function callQuerySsrServer<

// iterate keys and set the initialData
// generalize that to arbitrary depth, reduce right
const initialData = keys.reduceRight((acc, key) => {
// this adds the params to make a unique key
const initialData = keys.concat(paramsAsString).reduceRight((acc, key) => {
return {
[key]: acc,
};
}, result) as SsrQueryShape<typeof appRouter>;

// nest result based on keys
// keys = ['awardRouter', 'getActiveProfile']

// TODO: need to link the initial data to the the params also

// console.log("useQuery", { keys, initialData, initialDataForProc });

return initialData;
}

Expand Down

0 comments on commit 00345e8

Please sign in to comment.