Skip to content

Commit

Permalink
feat(react-query): allow for external apps in `createServerSideHelp…
Browse files Browse the repository at this point in the history
…ers` (#4547)


Co-authored-by: EmericW <emericwarnez@bitsoflove.be>
Co-authored-by: Emeric Warnez <emericwarnez@Emerics-MacBook-Pro.local>
  • Loading branch information
3 people committed Jun 22, 2023
1 parent 6a7096d commit 5fa756a
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 80 deletions.
20 changes: 17 additions & 3 deletions examples/next-prisma-todomvc/src/server/ssg-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@ import { i18n } from 'next-i18next.config';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import SuperJSON from 'superjson';
import { createInnerTRPCContext } from './context';
import { appRouter } from './routers/_app';
import { AppRouter, appRouter } from './routers/_app';

export async function ssgInit<TParams extends { locale?: string }>(
opts: GetStaticPropsContext<TParams>,
) {
// Using an external TRPC app
// const client = createTRPCProxyClient<AppRouter>({
// links: [
// httpBatchLink({
// url: 'http://localhost:3000/api/trpc',
// }),
// ],
// transformer: SuperJSON,
// });

// const ssg = createServerSideHelpers({
// client,
// })

const locale = opts.params?.locale ?? opts?.locale ?? i18n.defaultLocale;
const _i18n = await serverSideTranslations(locale, ['common']);

const ssg = createServerSideHelpers({
const ssg = createServerSideHelpers<AppRouter>({
router: appRouter,
transformer: SuperJSON,
ctx: await createInnerTRPCContext({
locale,
i18n: _i18n,
}),
transformer: SuperJSON,
});

// Prefetch i18n everytime
Expand Down
7 changes: 6 additions & 1 deletion packages/react-query/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export { createServerSideHelpers } from './ssgProxy';
export type { CreateSSGHelpersOptions } from './types';
export type {
/**
* @deprecated this exported is planned to be removed in the next major version
*/
CreateServerSideHelpersOptions as CreateSSGHelpersOptions,
} from './types';

export type { DecoratedProcedureSSGRecord } from './ssgProxy';
24 changes: 8 additions & 16 deletions packages/react-query/src/server/ssgProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
inferTransformedProcedureOutput,
} from '@trpc/server/shared';
import { createSSGHelpers } from '../ssg/ssg';
import { CreateSSGHelpersOptions } from './types';
import { CreateServerSideHelpersOptions } from './types';

type DecorateProcedure<TProcedure extends AnyProcedure> = {
/**
Expand Down Expand Up @@ -63,9 +63,10 @@ type AnyDecoratedProcedure = DecorateProcedure<any>;

/**
* Create functions you can use for server-side rendering / static generation
* @see https://trpc.io/docs/client/nextjs/server-side-helpers
*/
export function createServerSideHelpers<TRouter extends AnyRouter>(
opts: CreateSSGHelpersOptions<TRouter>,
opts: CreateServerSideHelpersOptions<TRouter>,
) {
const helpers = createSSGHelpers(opts);

Expand Down Expand Up @@ -94,20 +95,11 @@ export function createServerSideHelpers<TRouter extends AnyRouter>(

const fullPath = pathCopy.join('.');

switch (utilName) {
case 'fetch': {
return helpers.fetchQuery(fullPath, ...(args as any));
}
case 'fetchInfinite': {
return helpers.fetchInfiniteQuery(fullPath, ...(args as any));
}
case 'prefetch': {
return helpers.prefetchQuery(fullPath, ...(args as any));
}
case 'prefetchInfinite': {
return helpers.prefetchInfiniteQuery(fullPath, ...(args as any));
}
}
const helperKey = `${utilName}Query` as const;
// ^?

const fn: (...args: any) => any = helpers[helperKey];
return fn(fullPath, ...args);
});
});
}
15 changes: 12 additions & 3 deletions packages/react-query/src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { inferRouterProxyClient } from '@trpc/client';
import {
AnyRouter,
ClientDataTransformerOptions,
inferRouterContext,
} from '@trpc/server';
import { CreateTRPCReactQueryClientConfig } from '../shared';

interface CreateSSGHelpersOptionsBase<TRouter extends AnyRouter> {
interface CreateSSGHelpersInternal<TRouter extends AnyRouter> {
router: TRouter;
ctx: inferRouterContext<TRouter>;
transformer?: ClientDataTransformerOptions;
}
export type CreateSSGHelpersOptions<TRouter extends AnyRouter> =
CreateSSGHelpersOptionsBase<TRouter> & CreateTRPCReactQueryClientConfig;

interface CreateSSGHelpersExternal<TRouter extends AnyRouter> {
client: inferRouterProxyClient<TRouter>;
}

export type CreateServerSideHelpersOptions<TRouter extends AnyRouter> = (
| CreateSSGHelpersInternal<TRouter>
| CreateSSGHelpersExternal<TRouter>
) &
CreateTRPCReactQueryClientConfig;
87 changes: 44 additions & 43 deletions packages/react-query/src/ssg/ssg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,58 @@ import {
DehydrateOptions,
InfiniteData,
} from '@tanstack/react-query';
import { getUntypedClient } from '@trpc/client';
import {
AnyRouter,
callProcedure,
inferHandlerInput,
inferProcedureOutput,
} from '@trpc/server';
import { getArrayQueryKey } from '../internals/getArrayQueryKey';
import { CreateSSGHelpersOptions } from '../server/types';
import { CreateServerSideHelpersOptions } from '../server/types';
import { getQueryClient } from '../shared';

/**
* Create functions you can use for server-side rendering / static generation
* @deprecated use `createServerSideHelpers` instead
*/
export function createSSGHelpers<TRouter extends AnyRouter>(
opts: CreateSSGHelpersOptions<TRouter>,
opts: CreateServerSideHelpersOptions<TRouter>,
) {
const { router, transformer, ctx } = opts;
type TQueries = TRouter['_def']['queries'];
const queryClient = getQueryClient(opts);

const serialize = transformer
? ('input' in transformer ? transformer.input : transformer).serialize
: (obj: unknown) => obj;
const resolvedOpts: {
serialize: (obj: unknown) => any;
query: (queryOpts: { path: string; input: unknown }) => Promise<unknown>;
} = (() => {
if ('router' in opts) {
const { transformer, ctx, router } = opts;
return {
serialize: transformer
? ('input' in transformer ? transformer.input : transformer).serialize
: (obj) => obj,
query: (queryOpts) => {
return callProcedure({
procedures: router._def.procedures,
path: queryOpts.path,
rawInput: queryOpts.input,
ctx,
type: 'query',
});
},
};
}

const { client } = opts;
const untypedClient = getUntypedClient(client);

return {
query: (queryOpts) =>
untypedClient.query(queryOpts.path, queryOpts.input),
serialize: (obj) => untypedClient.runtime.transformer.serialize(obj),
};
})();

const prefetchQuery = async <
TPath extends keyof TQueries & string,
Expand All @@ -37,15 +65,8 @@ export function createSSGHelpers<TRouter extends AnyRouter>(
) => {
return queryClient.prefetchQuery({
queryKey: getArrayQueryKey(pathAndInput, 'query'),
queryFn: () => {
return callProcedure({
procedures: router._def.procedures,
path: pathAndInput[0],
rawInput: pathAndInput[1],
ctx,
type: 'query',
});
},
queryFn: () =>
resolvedOpts.query({ path: pathAndInput[0], input: pathAndInput[1] }),
});
};

Expand All @@ -57,15 +78,8 @@ export function createSSGHelpers<TRouter extends AnyRouter>(
) => {
return queryClient.prefetchInfiniteQuery({
queryKey: getArrayQueryKey(pathAndInput, 'infinite'),
queryFn: () => {
return callProcedure({
procedures: router._def.procedures,
path: pathAndInput[0],
rawInput: pathAndInput[1],
ctx,
type: 'query',
});
},
queryFn: () =>
resolvedOpts.query({ path: pathAndInput[0], input: pathAndInput[1] }),
});
};

Expand All @@ -78,15 +92,8 @@ export function createSSGHelpers<TRouter extends AnyRouter>(
): Promise<TOutput> => {
return queryClient.fetchQuery({
queryKey: getArrayQueryKey(pathAndInput, 'query'),
queryFn: () => {
return callProcedure({
procedures: router._def.procedures,
path: pathAndInput[0],
rawInput: pathAndInput[1],
ctx,
type: 'query',
});
},
queryFn: () =>
resolvedOpts.query({ path: pathAndInput[0], input: pathAndInput[1] }),
});
};

Expand All @@ -99,15 +106,8 @@ export function createSSGHelpers<TRouter extends AnyRouter>(
): Promise<InfiniteData<TOutput>> => {
return queryClient.fetchInfiniteQuery({
queryKey: getArrayQueryKey(pathAndInput, 'infinite'),
queryFn: () => {
return callProcedure({
procedures: router._def.procedures,
path: pathAndInput[0],
rawInput: pathAndInput[1],
ctx,
type: 'query',
});
},
queryFn: () =>
resolvedOpts.query({ path: pathAndInput[0], input: pathAndInput[1] }),
});
};

Expand All @@ -120,7 +120,8 @@ export function createSSGHelpers<TRouter extends AnyRouter>(
},
): DehydratedState {
const before = dehydrate(queryClient, opts);
const after = serialize(before);
const after = resolvedOpts.serialize(before);

return after;
}

Expand Down
50 changes: 39 additions & 11 deletions packages/tests/server/react/createClient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ const ctx = konn()
});

const queryClient = createQueryClient();
const proxy = createTRPCReact<typeof appRouter>();
const hooks = createTRPCReact<typeof appRouter>();
const opts = routerToServerAndClientNew(appRouter);

function App(props: { children: ReactNode }) {
const [client] = useState(() =>
proxy.createClient({
hooks.createClient({
links: [
httpBatchLink({
url: opts.httpUrl,
Expand All @@ -30,24 +30,24 @@ const ctx = konn()
}),
);
return (
<proxy.Provider {...{ queryClient, client }}>
<hooks.Provider {...{ queryClient, client }}>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</proxy.Provider>
</hooks.Provider>
);
}
return { ...opts, proxy, App };
return { ...opts, hooks, App };
})
.afterEach(async (ctx) => {
await ctx?.close?.();
})
.done();

test('createClient()', async () => {
const { App, proxy } = ctx;
const { App, hooks } = ctx;
function MyComponent() {
const query1 = proxy.hello.useQuery();
const query1 = hooks.hello.useQuery();

if (!query1.data) {
return <>...</>;
Expand All @@ -67,18 +67,46 @@ test('createClient()', async () => {
});
});

test('useDehydratedState()', async () => {
const { App, proxy, router } = ctx;
test('useDehydratedState() - internal', async () => {
const { App, hooks, router } = ctx;

const ssg = createServerSideHelpers({ router, ctx: {} });
const res = await ssg.hello.fetch();
expect(res).toBe('world');
const dehydratedState = ssg.dehydrate();

function MyComponent() {
const utils = proxy.useContext();
const utils = hooks.useContext();

const state = proxy.useDehydratedState(utils.client, dehydratedState);
const state = hooks.useDehydratedState(utils.client, dehydratedState);
return <h1>{JSON.stringify(state)}</h1>;
}

const utils = render(
<App>
<MyComponent />
</App>,
);

await waitFor(() => {
expect(utils.container).toHaveTextContent('world');
});
});

test('useDehydratedState() - external', async () => {
const { App, hooks, proxy } = ctx;

const ssg = createServerSideHelpers({ client: proxy });
const res = await ssg.hello.fetch();
expect(res).toBe('world');
expectTypeOf(res).toMatchTypeOf<string>();

const dehydratedState = ssg.dehydrate();

function MyComponent() {
const utils = hooks.useContext();

const state = hooks.useDehydratedState(utils.client, dehydratedState);
return <h1>{JSON.stringify(state)}</h1>;
}

Expand Down
Loading

3 comments on commit 5fa756a

@vercel
Copy link

@vercel vercel bot commented on 5fa756a Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

www – ./www

alpha.trpc.io
www-git-main-trpc.vercel.app
www.trpc.io
beta.trpc.io
trpc.io
www-trpc.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 5fa756a Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

next-prisma-starter – ./examples/next-prisma-starter

nextjs.trpc.io
next-prisma-starter-trpc.vercel.app
next-prisma-starter-git-main-trpc.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 5fa756a Jun 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

og-image – ./www/og-image

og-image-git-main-trpc.vercel.app
og-image-trpc.vercel.app
og-image-three-neon.vercel.app
og-image.trpc.io

Please sign in to comment.