From d61ece329f271b69c16a6a262c1979259b7fd366 Mon Sep 17 00:00:00 2001 From: Alex / KATT Date: Fri, 2 Feb 2024 14:42:18 +0100 Subject: [PATCH] fix(next): add `ssrPrepass`-callback to avoid bundling of `react-dom` (#5426) --- .gitignore | 1 - .../diagnostics-big-router/src/utils/trpc.ts | 1 - examples/next-big-router/src/utils/trpc.ts | 1 - examples/next-edge-runtime/src/utils/trpc.ts | 1 - .../next-minimal-starter/src/utils/trpc.ts | 2 + .../playwright/smoke.test.ts | 38 +-- .../next-prisma-starter/src/utils/trpc.ts | 28 +-- .../src/utils/trpc.ts | 10 +- packages/next/README.md | 2 +- packages/next/package.json | 6 + packages/next/rollup.config.ts | 1 + packages/next/src/ssrPrepass.ts | 185 +++++++++++++++ packages/next/src/withTRPC.tsx | 216 +++--------------- packages/next/turbo.json | 4 +- .../issue-1645-setErrorStatusSSR.test.tsx | 2 + packages/tests/server/react/withTRPC.test.tsx | 21 ++ ...e-4130-ssr-different-transformers.test.tsx | 2 + www/docs/client/nextjs/setup.mdx | 15 -- www/docs/client/nextjs/ssr.md | 4 +- .../migration/migrate-from-v10-to-v11.mdx | 6 + 20 files changed, 279 insertions(+), 267 deletions(-) create mode 100644 packages/next/src/ssrPrepass.ts diff --git a/.gitignore b/.gitignore index b9d2a7d9540..7f9778e28e3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ yarn.lock yalc.lock coverage/ -playwright/ __generated__/ diff --git a/examples/.test/diagnostics-big-router/src/utils/trpc.ts b/examples/.test/diagnostics-big-router/src/utils/trpc.ts index ad2631978b3..73ee11f9342 100644 --- a/examples/.test/diagnostics-big-router/src/utils/trpc.ts +++ b/examples/.test/diagnostics-big-router/src/utils/trpc.ts @@ -30,5 +30,4 @@ export const trpc = createTRPCNext({ ], }; }, - ssr: true, }); diff --git a/examples/next-big-router/src/utils/trpc.ts b/examples/next-big-router/src/utils/trpc.ts index 3d4c9ca2ee1..4618eacbd08 100644 --- a/examples/next-big-router/src/utils/trpc.ts +++ b/examples/next-big-router/src/utils/trpc.ts @@ -28,5 +28,4 @@ export const trpc = createTRPCNext({ ], }; }, - ssr: true, }); diff --git a/examples/next-edge-runtime/src/utils/trpc.ts b/examples/next-edge-runtime/src/utils/trpc.ts index 49abdc58eb3..ddfb82eb8fa 100644 --- a/examples/next-edge-runtime/src/utils/trpc.ts +++ b/examples/next-edge-runtime/src/utils/trpc.ts @@ -28,5 +28,4 @@ export const trpc = createTRPCNext({ ], }; }, - ssr: true, }); diff --git a/examples/next-minimal-starter/src/utils/trpc.ts b/examples/next-minimal-starter/src/utils/trpc.ts index 49abdc58eb3..a3b4b600fe7 100644 --- a/examples/next-minimal-starter/src/utils/trpc.ts +++ b/examples/next-minimal-starter/src/utils/trpc.ts @@ -1,5 +1,6 @@ import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { AppRouter } from '../pages/api/trpc/[trpc]'; function getBaseUrl() { @@ -29,4 +30,5 @@ export const trpc = createTRPCNext({ }; }, ssr: true, + ssrPrepass, }); diff --git a/examples/next-prisma-starter/playwright/smoke.test.ts b/examples/next-prisma-starter/playwright/smoke.test.ts index b8b49d52d4b..9a2dd3728df 100644 --- a/examples/next-prisma-starter/playwright/smoke.test.ts +++ b/examples/next-prisma-starter/playwright/smoke.test.ts @@ -1,4 +1,4 @@ -import { test, expect } from '@playwright/test'; +import { test } from '@playwright/test'; test.setTimeout(35e3); @@ -8,12 +8,7 @@ test('go to /', async ({ page }) => { await page.waitForSelector(`text=Starter`); }); -test('test 404', async ({ page }) => { - const res = await page.goto('/post/not-found'); - expect(res?.status()).toBe(404); -}); - -test('add a post', async ({ page, browser }) => { +test('add a post', async ({ page }) => { const nonce = `${Math.random()}`; await page.goto('/'); @@ -23,32 +18,5 @@ test('add a post', async ({ page, browser }) => { await page.waitForLoadState('networkidle'); await page.reload(); - expect(await page.content()).toContain(nonce); - - const ssrContext = await browser.newContext({ - javaScriptEnabled: false, - }); - const ssrPage = await ssrContext.newPage(); - await ssrPage.goto('/'); - - expect(await ssrPage.content()).toContain(nonce); -}); - -test('server-side rendering test', async ({ page, browser }) => { - // add a post - const nonce = `${Math.random()}`; - - await page.goto('/'); - await page.fill(`[name=title]`, nonce); - await page.fill(`[name=text]`, nonce); - await page.click(`form [type=submit]`); - await page.waitForLoadState('networkidle'); - - // load the page without js - const ssrContext = await browser.newContext({ - javaScriptEnabled: false, - }); - const ssrPage = await ssrContext.newPage(); - await ssrPage.goto('/'); - expect(await ssrPage.content()).toContain(nonce); + await page.waitForSelector(`text="${nonce}"`); }); diff --git a/examples/next-prisma-starter/src/utils/trpc.ts b/examples/next-prisma-starter/src/utils/trpc.ts index f03d1b94457..fa455f8bae3 100644 --- a/examples/next-prisma-starter/src/utils/trpc.ts +++ b/examples/next-prisma-starter/src/utils/trpc.ts @@ -1,5 +1,6 @@ import { httpBatchLink, loggerLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; + import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import type { NextPageContext } from 'next'; // ℹ️ Type-only import: @@ -96,32 +97,7 @@ export const trpc = createTRPCNext({ /** * @link https://trpc.io/docs/v11/ssr */ - ssr: true, - /** - * Set headers or status code when doing SSR - */ - responseMeta(opts) { - const ctx = opts.ctx as SSRContext; - - if (ctx.status) { - // If HTTP status set, propagate that - return { - status: ctx.status, - }; - } - - const error = opts.clientErrors[0]; - if (error) { - // Propagate http first error from API calls - return { - status: error.data?.httpStatus ?? 500, - }; - } - - // for app caching with SSR see https://trpc.io/docs/v11/caching - - return {}; - }, + ssr: false, /** * @link https://trpc.io/docs/v11/data-transformers */ diff --git a/examples/next-prisma-websockets-starter/src/utils/trpc.ts b/examples/next-prisma-websockets-starter/src/utils/trpc.ts index 1b7e3a6d4c9..e37255da33d 100644 --- a/examples/next-prisma-websockets-starter/src/utils/trpc.ts +++ b/examples/next-prisma-websockets-starter/src/utils/trpc.ts @@ -6,6 +6,7 @@ import { wsLink, } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { inferRouterOutputs } from '@trpc/server'; import type { NextPageContext } from 'next'; import getConfig from 'next/config'; @@ -56,6 +57,11 @@ function getEndingLink(ctx: NextPageContext | undefined): TRPCLink { * @link https://trpc.io/docs/v11/react#3-create-trpc-hooks */ export const trpc = createTRPCNext({ + /** + * @link https://trpc.io/docs/v11/ssr + */ + ssr: true, + ssrPrepass, config({ ctx }) { /** * If you want to use SSR, you need to use the server's full URL @@ -82,10 +88,6 @@ export const trpc = createTRPCNext({ queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } }, }; }, - /** - * @link https://trpc.io/docs/v11/ssr - */ - ssr: true, /** * @link https://trpc.io/docs/v11/data-transformers */ diff --git a/packages/next/README.md b/packages/next/README.md index b88fc8e0885..e69f1c0c1c8 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -55,7 +55,7 @@ export const trpc = createTRPCNext({ ], }; }, - ssr: true, + ssr: false, }); ``` diff --git a/packages/next/package.json b/packages/next/package.json index 828869400ad..053d163dbf3 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -55,6 +55,11 @@ "import": "./dist/app-dir/server.mjs", "require": "./dist/app-dir/server.js", "default": "./dist/app-dir/server.js" + }, + "./ssrPrepass": { + "import": "./dist/ssrPrepass.mjs", + "require": "./dist/ssrPrepass.js", + "default": "./dist/ssrPrepass.js" } }, "files": [ @@ -63,6 +68,7 @@ "README.md", "package.json", "app-dir", + "ssrPrepass", "!**/*.test.*" ], "dependencies": {}, diff --git a/packages/next/rollup.config.ts b/packages/next/rollup.config.ts index 73c230e86aa..1f250f7e9f2 100644 --- a/packages/next/rollup.config.ts +++ b/packages/next/rollup.config.ts @@ -8,6 +8,7 @@ export const input = [ 'src/app-dir/client.ts', 'src/app-dir/links/nextCache.ts', 'src/app-dir/links/nextHttp.ts', + 'src/ssrPrepass.ts', ]; export default function rollup(): RollupOptions[] { diff --git a/packages/next/src/ssrPrepass.ts b/packages/next/src/ssrPrepass.ts new file mode 100644 index 00000000000..afea848f35f --- /dev/null +++ b/packages/next/src/ssrPrepass.ts @@ -0,0 +1,185 @@ +/** + * Heavily based on urql's ssr + * https://github.com/FormidableLabs/urql/blob/main/packages/next-urql/src/with-urql-client.ts + */ +import type { DehydratedState } from '@tanstack/react-query'; +import { dehydrate } from '@tanstack/react-query'; +import { createTRPCUntypedClient } from '@trpc/client'; +import type { CoercedTransformerParameters } from '@trpc/client/unstable-internals'; +import { getTransformer } from '@trpc/client/unstable-internals'; +import type { TRPCClientError, TRPCClientErrorLike } from '@trpc/react-query'; +import { getQueryClient } from '@trpc/react-query/shared'; +import type { + AnyRouter, + Dict, + Maybe, +} from '@trpc/server/unstable-core-do-not-import'; +import type { + AppContextType, + NextPageContext, +} from 'next/dist/shared/lib/utils'; +import { createElement } from 'react'; +import type { TRPCPrepassHelper, TRPCPrepassProps } from './withTRPC'; + +function transformQueryOrMutationCacheErrors< + TState extends + | DehydratedState['mutations'][0] + | DehydratedState['queries'][0], +>(result: TState): TState { + const error = result.state.error as Maybe>; + if (error instanceof Error && error.name === 'TRPCClientError') { + const newError: TRPCClientErrorLike = { + message: error.message, + data: error.data, + shape: error.shape, + }; + return { + ...result, + state: { + ...result.state, + error: newError, + }, + }; + } + return result; +} + +export const ssrPrepass: TRPCPrepassHelper = (opts) => { + const { parent, WithTRPC, AppOrPage } = opts; + type $PrepassProps = TRPCPrepassProps; + + const transformer = getTransformer( + (parent as CoercedTransformerParameters).transformer, + ); + WithTRPC.getInitialProps = async (appOrPageCtx: AppContextType) => { + const shouldSsr = async () => { + if (typeof window !== 'undefined') { + return false; + } + if (typeof parent.ssr === 'function') { + try { + return await parent.ssr({ ctx: appOrPageCtx.ctx }); + } catch (e) { + return false; + } + } + return parent.ssr; + }; + const ssrEnabled = await shouldSsr(); + const AppTree = appOrPageCtx.AppTree; + + // Determine if we are wrapping an App component or a Page component. + const isApp = !!appOrPageCtx.Component; + const ctx: NextPageContext = isApp + ? appOrPageCtx.ctx + : (appOrPageCtx as any as NextPageContext); + + // Run the wrapped component's getInitialProps function. + let pageProps: Dict = {}; + if (AppOrPage.getInitialProps) { + const originalProps = await AppOrPage.getInitialProps( + appOrPageCtx as any, + ); + const originalPageProps = isApp + ? originalProps.pageProps ?? {} + : originalProps; + + pageProps = { + ...originalPageProps, + ...pageProps, + }; + } + const getAppTreeProps = (props: Record) => + isApp ? { pageProps: props } : props; + + if (typeof window !== 'undefined' || !ssrEnabled) { + return getAppTreeProps(pageProps); + } + + const config = parent.config({ ctx }); + const trpcClient = createTRPCUntypedClient(config); + const queryClient = getQueryClient(config); + + const trpcProp: $PrepassProps = { + config, + trpcClient, + queryClient, + ssrState: 'prepass', + ssrContext: ctx, + }; + const prepassProps = { + pageProps, + trpc: trpcProp, + }; + + const reactDomServer = await import('react-dom/server'); + + // Run the prepass step on AppTree. This will run all trpc queries on the server. + // multiple prepass ensures that we can do batching on the server + while (true) { + // render full tree + reactDomServer.renderToString(createElement(AppTree, prepassProps)); + if (!queryClient.isFetching()) { + // the render didn't cause the queryClient to fetch anything + break; + } + + // wait until the query cache has settled it's promises + await new Promise((resolve) => { + const unsub = queryClient.getQueryCache().subscribe((event) => { + if (event?.query.getObserversCount() === 0) { + resolve(); + unsub(); + } + }); + }); + } + const dehydratedCache = dehydrate(queryClient, { + shouldDehydrateQuery(query) { + // filter out queries that are marked as trpc: { ssr: false } or are not enabled, but make sure errors are dehydrated + const isExcludedFromSSr = + query.state.fetchStatus === 'idle' && + query.state.status === 'pending'; + return !isExcludedFromSSr; + }, + }); + // since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects + const dehydratedCacheWithErrors = { + ...dehydratedCache, + queries: dehydratedCache.queries.map(transformQueryOrMutationCacheErrors), + mutations: dehydratedCache.mutations.map( + transformQueryOrMutationCacheErrors, + ), + }; + + // dehydrate query client's state and add it to the props + pageProps['trpcState'] = transformer.input.serialize( + dehydratedCacheWithErrors, + ); + + const appTreeProps = getAppTreeProps(pageProps); + + const meta = + parent.responseMeta?.({ + ctx, + clientErrors: [...dehydratedCache.queries, ...dehydratedCache.mutations] + .map((v) => v.state.error) + .flatMap((err) => + err instanceof Error && err.name === 'TRPCClientError' + ? [err as TRPCClientError] + : [], + ), + }) ?? {}; + + for (const [key, value] of Object.entries(meta.headers ?? {})) { + if (typeof value === 'string') { + ctx.res?.setHeader(key, value); + } + } + if (meta.status && ctx.res) { + ctx.res.statusCode = meta.status; + } + + return appTreeProps; + }; +}; diff --git a/packages/next/src/withTRPC.tsx b/packages/next/src/withTRPC.tsx index 60cb23fe763..689e402ca71 100644 --- a/packages/next/src/withTRPC.tsx +++ b/packages/next/src/withTRPC.tsx @@ -3,19 +3,14 @@ * https://github.com/FormidableLabs/urql/blob/main/packages/next-urql/src/with-urql-client.ts */ import type { DehydratedState, QueryClient } from '@tanstack/react-query'; -import { - dehydrate, - HydrationBoundary, - QueryClientProvider, -} from '@tanstack/react-query'; +import { HydrationBoundary, QueryClientProvider } from '@tanstack/react-query'; import type { CreateTRPCClientOptions, TRPCUntypedClient } from '@trpc/client'; -import { createTRPCUntypedClient } from '@trpc/client'; import type { CoercedTransformerParameters } from '@trpc/client/unstable-internals'; import { getTransformer, type TransformerOptions, } from '@trpc/client/unstable-internals'; -import type { TRPCClientError, TRPCClientErrorLike } from '@trpc/react-query'; +import type { TRPCClientError } from '@trpc/react-query'; import type { CreateTRPCReactOptions, CreateTRPCReactQueryClientConfig, @@ -23,42 +18,17 @@ import type { import { createRootHooks, getQueryClient } from '@trpc/react-query/shared'; import type { AnyRouter, - Dict, inferRootTypes, - Maybe, ResponseMeta, } from '@trpc/server/unstable-core-do-not-import'; import type { - AppContextType, AppPropsType, NextComponentType, NextPageContext, } from 'next/dist/shared/lib/utils'; import type { NextRouter } from 'next/router'; -import React, { createElement, useState } from 'react'; +import React, { useState } from 'react'; -function transformQueryOrMutationCacheErrors< - TState extends - | DehydratedState['mutations'][0] - | DehydratedState['queries'][0], ->(result: TState): TState { - const error = result.state.error as Maybe>; - if (error instanceof Error && error.name === 'TRPCClientError') { - const newError: TRPCClientErrorLike = { - message: error.message, - data: error.data, - shape: error.shape, - }; - return { - ...result, - state: { - ...result.state, - error: newError, - }, - }; - } - return result; -} export type WithTRPCConfig = CreateTRPCClientOptions & CreateTRPCReactQueryClientConfig & { @@ -70,8 +40,17 @@ type WithTRPCOptions = config: (info: { ctx?: NextPageContext }) => WithTRPCConfig; } & TransformerOptions>; +export type TRPCPrepassHelper = (opts: { + parent: WithTRPCSSROptions; + WithTRPC: NextComponentType; + AppOrPage: NextComponentType; +}) => void; export type WithTRPCSSROptions = WithTRPCOptions & { + /** + * If you enable this, you also need to add a `ssrPrepass`-prop + * @link https://trpc.io/docs/client/nextjs/ssr + */ ssr: | true | ((opts: { ctx: NextPageContext }) => boolean | Promise); @@ -79,6 +58,11 @@ export type WithTRPCSSROptions = ctx: NextPageContext; clientErrors: TRPCClientError[]; }) => ResponseMeta; + /** + * use `import { ssrPrepass } from '@trpc/next/ssrPrepass'` + * @link https://trpc.io/docs/client/nextjs/ssr + */ + ssrPrepass: TRPCPrepassHelper; }; export type WithTRPCNoSSROptions = @@ -86,6 +70,17 @@ export type WithTRPCNoSSROptions = ssr?: false; }; +export type TRPCPrepassProps< + TRouter extends AnyRouter, + TSSRContext extends NextPageContext = NextPageContext, +> = { + config: WithTRPCConfig; + queryClient: QueryClient; + trpcClient: TRPCUntypedClient; + ssrState: 'prepass'; + ssrContext: TSSRContext; +}; + export function withTRPC< TRouter extends AnyRouter, TSSRContext extends NextPageContext = NextPageContext, @@ -95,19 +90,13 @@ export function withTRPC< (opts as CoercedTransformerParameters).transformer, ); - type TRPCPrepassProps = { - config: WithTRPCConfig; - queryClient: QueryClient; - trpcClient: TRPCUntypedClient; - ssrState: 'prepass'; - ssrContext: TSSRContext; - }; + type $PrepassProps = TRPCPrepassProps; return (AppOrPage: NextComponentType): NextComponentType => { const trpc = createRootHooks(opts); const WithTRPC = ( props: AppPropsType & { - trpc?: TRPCPrepassProps; + trpc?: $PrepassProps; }, ) => { const [prepassProps] = useState(() => { @@ -159,145 +148,12 @@ export function withTRPC< ); }; - if (AppOrPage.getInitialProps ?? opts.ssr) { - WithTRPC.getInitialProps = async (appOrPageCtx: AppContextType) => { - const shouldSsr = async () => { - if (typeof opts.ssr === 'function') { - if (typeof window !== 'undefined') { - return false; - } - try { - return await opts.ssr({ ctx: appOrPageCtx.ctx }); - } catch (e) { - return false; - } - } - return opts.ssr; - }; - const ssr = await shouldSsr(); - const AppTree = appOrPageCtx.AppTree; - - // Determine if we are wrapping an App component or a Page component. - const isApp = !!appOrPageCtx.Component; - const ctx: NextPageContext = isApp - ? appOrPageCtx.ctx - : (appOrPageCtx as any as NextPageContext); - - // Run the wrapped component's getInitialProps function. - let pageProps: Dict = {}; - if (AppOrPage.getInitialProps) { - const originalProps = await AppOrPage.getInitialProps( - appOrPageCtx as any, - ); - const originalPageProps = isApp - ? originalProps.pageProps ?? {} - : originalProps; - - pageProps = { - ...originalPageProps, - ...pageProps, - }; - } - const getAppTreeProps = (props: Record) => - isApp ? { pageProps: props } : props; - - if (typeof window !== 'undefined' || !ssr) { - return getAppTreeProps(pageProps); - } - - const config = getClientConfig({ ctx }); - const trpcClient = createTRPCUntypedClient(config); - const queryClient = getQueryClient(config); - - const trpcProp: TRPCPrepassProps = { - config, - trpcClient, - queryClient, - ssrState: 'prepass', - ssrContext: ctx as TSSRContext, - }; - const prepassProps = { - pageProps, - trpc: trpcProp, - }; - - const reactDomServer = await import('react-dom/server'); - - // Run the prepass step on AppTree. This will run all trpc queries on the server. - // multiple prepass ensures that we can do batching on the server - while (true) { - // render full tree - reactDomServer.renderToString(createElement(AppTree, prepassProps)); - if (!queryClient.isFetching()) { - // the render didn't cause the queryClient to fetch anything - break; - } - - // wait until the query cache has settled it's promises - await new Promise((resolve) => { - const unsub = queryClient.getQueryCache().subscribe((event) => { - if (event?.query.getObserversCount() === 0) { - resolve(); - unsub(); - } - }); - }); - } - const dehydratedCache = dehydrate(queryClient, { - shouldDehydrateQuery(query) { - // filter out queries that are marked as trpc: { ssr: false } or are not enabled, but make sure errors are dehydrated - const isExcludedFromSSr = - query.state.fetchStatus === 'idle' && - query.state.status === 'pending'; - return !isExcludedFromSSr; - }, - }); - // since error instances can't be serialized, let's make them into `TRPCClientErrorLike`-objects - const dehydratedCacheWithErrors = { - ...dehydratedCache, - queries: dehydratedCache.queries.map( - transformQueryOrMutationCacheErrors, - ), - mutations: dehydratedCache.mutations.map( - transformQueryOrMutationCacheErrors, - ), - }; - - // dehydrate query client's state and add it to the props - pageProps['trpcState'] = transformer.input.serialize( - dehydratedCacheWithErrors, - ); - - const appTreeProps = getAppTreeProps(pageProps); - - if ('responseMeta' in opts) { - const meta = - opts.responseMeta?.({ - ctx, - clientErrors: [ - ...dehydratedCache.queries, - ...dehydratedCache.mutations, - ] - .map((v) => v.state.error) - .flatMap((err) => - err instanceof Error && err.name === 'TRPCClientError' - ? [err as TRPCClientError] - : [], - ), - }) ?? {}; - - for (const [key, value] of Object.entries(meta.headers ?? {})) { - if (typeof value === 'string') { - ctx.res?.setHeader(key, value); - } - } - if (meta.status && ctx.res) { - ctx.res.statusCode = meta.status; - } - } - - return appTreeProps; - }; + if (opts.ssr) { + opts.ssrPrepass({ + parent: opts, + AppOrPage, + WithTRPC, + }); } const displayName = AppOrPage.displayName ?? AppOrPage.name ?? 'Component'; diff --git a/packages/next/turbo.json b/packages/next/turbo.json index 03cee29e9d4..024f6eeaf47 100644 --- a/packages/next/turbo.json +++ b/packages/next/turbo.json @@ -2,6 +2,8 @@ "$schema": "https://turborepo.org/schema.json", "extends": ["//"], "pipeline": { - "codegen-entrypoints": { "outputs": ["package.json", "app-dir/**"] } + "codegen-entrypoints": { + "outputs": ["package.json", "app-dir/**", "ssrPrepass/**"] + } } } diff --git a/packages/tests/server/react/regression/issue-1645-setErrorStatusSSR.test.tsx b/packages/tests/server/react/regression/issue-1645-setErrorStatusSSR.test.tsx index 87a8a9f300d..9f395e17e5a 100644 --- a/packages/tests/server/react/regression/issue-1645-setErrorStatusSSR.test.tsx +++ b/packages/tests/server/react/regression/issue-1645-setErrorStatusSSR.test.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { createAppRouter } from '../__testHelpers'; import { withTRPC } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import type { AppType } from 'next/dist/shared/lib/utils'; import React from 'react'; @@ -44,6 +45,7 @@ test('regression: SSR with error sets `status`=`error`', async () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); await Wrapped.getInitialProps!({ diff --git a/packages/tests/server/react/withTRPC.test.tsx b/packages/tests/server/react/withTRPC.test.tsx index 34e7b9b420a..228bc3a6433 100644 --- a/packages/tests/server/react/withTRPC.test.tsx +++ b/packages/tests/server/react/withTRPC.test.tsx @@ -3,6 +3,7 @@ import { createAppRouter } from './__testHelpers'; import type { DehydratedState } from '@tanstack/react-query'; import { render, waitFor } from '@testing-library/react'; import { withTRPC } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import { konn } from 'konn'; import type { AppType, NextPageContext } from 'next/dist/shared/lib/utils'; import React from 'react'; @@ -29,6 +30,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -63,6 +65,7 @@ describe('withTRPC()', () => { ssr: ({ ctx }) => { return ctx?.pathname === '/'; }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -96,6 +99,7 @@ describe('withTRPC()', () => { ssr: () => { throw new Error('oops'); }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -129,6 +133,7 @@ describe('withTRPC()', () => { ssr: ({ ctx }) => { return ctx?.pathname === '/not-matching-path'; }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -163,6 +168,7 @@ describe('withTRPC()', () => { await new Promise((resolve) => setTimeout(resolve, 100)); return ctx?.pathname === '/'; }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -188,6 +194,7 @@ describe('withTRPC()', () => { ssr: async () => { return true; }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -232,6 +239,7 @@ describe('withTRPC()', () => { ssr: async ({ ctx }) => { return ctx?.pathname === '/'; }, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -263,6 +271,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: ssrFn, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -297,6 +306,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -369,6 +379,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -392,6 +403,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -423,6 +435,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -465,6 +478,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -501,6 +515,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -544,6 +559,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -589,6 +605,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = (await Wrapped.getInitialProps!({ @@ -641,6 +658,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -696,6 +714,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -749,6 +768,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); const props = await Wrapped.getInitialProps!({ @@ -815,6 +835,7 @@ describe('withTRPC()', () => { const Wrapped = withTRPC({ config: () => trpcClientOptions, ssr: true, + ssrPrepass, })(App); // @ts-ignore diff --git a/packages/tests/server/regression/issue-4130-ssr-different-transformers.test.tsx b/packages/tests/server/regression/issue-4130-ssr-different-transformers.test.tsx index c76a5192442..e32595311e4 100644 --- a/packages/tests/server/regression/issue-4130-ssr-different-transformers.test.tsx +++ b/packages/tests/server/regression/issue-4130-ssr-different-transformers.test.tsx @@ -3,6 +3,7 @@ import { routerToServerAndClientNew } from '../___testHelpers'; import type { DehydratedState } from '@tanstack/react-query'; import { createTRPCNext } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import { initTRPC } from '@trpc/server'; import type { CombinedDataTransformer } from '@trpc/server/unstable-core-do-not-import'; import { uneval } from 'devalue'; @@ -56,6 +57,7 @@ test('withTRPC - SSR', async () => { }, ssr: true, transformer, + ssrPrepass, }); const App: AppType = () => { diff --git a/www/docs/client/nextjs/setup.mdx b/www/docs/client/nextjs/setup.mdx index c73a8a2bf60..36faa73bdf4 100644 --- a/www/docs/client/nextjs/setup.mdx +++ b/www/docs/client/nextjs/setup.mdx @@ -263,21 +263,6 @@ export const trpc = createTRPCNext({ config(opts) { /* [...] */ }, - ssr: true, - responseMeta(opts) { - const { clientErrors } = opts; - if (clientErrors.length) { - // propagate first http error from API calls - return { - status: clientErrors[0].data?.httpStatus ?? 500, - }; - } - // cache full page for 1 day + revalidate once every second - const ONE_DAY_IN_SECONDS = 60 * 60 * 24; - return { - 'Cache-Control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`, - }; - }, }); ``` diff --git a/www/docs/client/nextjs/ssr.md b/www/docs/client/nextjs/ssr.md index ecd9c92746a..1a593cedd33 100644 --- a/www/docs/client/nextjs/ssr.md +++ b/www/docs/client/nextjs/ssr.md @@ -21,10 +21,13 @@ Additionally, consider [`Response Caching`](../../server/caching.md). ```tsx title='utils/trpc.ts' import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; +import { ssrPrepass } from '@trpc/next/ssrPrepass'; import superjson from 'superjson'; import type { AppRouter } from './api/trpc/[trpc]'; export const trpc = createTRPCNext({ + ssr: true, + ssrPrepass, config(opts) { const { ctx } = opts; if (typeof window !== 'undefined') { @@ -61,7 +64,6 @@ export const trpc = createTRPCNext({ ], }; }, - ssr: true, }); ``` diff --git a/www/docs/migration/migrate-from-v10-to-v11.mdx b/www/docs/migration/migrate-from-v10-to-v11.mdx index cb971e9128e..f5fcbbbcead 100644 --- a/www/docs/migration/migrate-from-v10-to-v11.mdx +++ b/www/docs/migration/migrate-from-v10-to-v11.mdx @@ -19,6 +19,12 @@ import { InstallSnippet } from '@site/src/components/InstallSnippet'; +## `@trpc/next` ssr mode now requires a prepass helper with `ssr: true` + +This is to fix https://github.com/trpc/trpc/issues/5378 where `react-dom` was imported regardless if you were using this functionality or not. + +See [SSR docs](../client/nextjs/ssr.md) + ## Reverse-chronological changelog > This is a draft document. It will be updated to a proper guide as we get closer to the v11 release.