diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/RefTestChild.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/RefTestChild.tsx deleted file mode 100644 index 37365db0..00000000 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/RefTestChild.tsx +++ /dev/null @@ -1,103 +0,0 @@ -"use client"; - -import { QueryRef, useQueryRefHandlers } from "@apollo/client"; -import { DynamicProductResult } from "@integration-test/shared/queries"; -import { useEffect, useState } from "react"; -import { - InternalQueryReference, - unwrapQueryRef, -} from "@apollo/client/react/internal"; - -import { TransportedQueryRef } from "@apollo/experimental-nextjs-app-support"; - -declare global { - interface Window { - testRefs?: { - distinctObjectReferences: Set; - uniqueQueryRefs1: Set>; - uniqueQueryRefs2: Set>; - distinctQueryRefs: Set>; - }; - } -} -export function RefTestChild({ - queryRef, - set, -}: { - queryRef: TransportedQueryRef; - set: "1" | "2"; -}) { - const [isClient, setIsClient] = useState(false); - - useQueryRefHandlers(queryRef); - - useEffect(() => { - const realQueryRef = queryRef as any as { - __transportedQueryRef: QueryRef; - }; - if (!window.testRefs) { - window.testRefs = { - distinctObjectReferences: new Set(), // expected: [transportedQueryRef1_1, transportedQueryRef1_2, transportedQueryRef2_1, transportedQueryRef2_2] - distinctQueryRefs: new Set(), // expected: [innerQueryRef1, innerQueryRef2] - uniqueQueryRefs1: new Set(), // expected: [innerQueryRef1] - uniqueQueryRefs2: new Set(), // expected: [innerQueryRef2] - }; - } - window.testRefs[`uniqueQueryRefs${set}`].add( - unwrapQueryRef(realQueryRef.__transportedQueryRef)! - ); - window.testRefs.distinctQueryRefs.add( - unwrapQueryRef(realQueryRef.__transportedQueryRef)! - ); - window.testRefs.distinctObjectReferences.add(queryRef); - setIsClient(true); - }, []); - - return isClient && window.testRefs ? ( - <> -
- -
-
- -
-
- -
- - ) : null; -} diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/page.tsx b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/page.tsx deleted file mode 100644 index 0d933a78..00000000 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/page.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { QUERY } from "@integration-test/shared/queries"; -import { Suspense } from "react"; -import { PreloadQuery } from "@/app/rsc/client"; -import { RefTestChild } from "./RefTestChild"; -import { ApolloWrapper } from "@/app/cc/ApolloWrapper"; - -import "./styles.css"; - -export const dynamic = "force-dynamic"; - -export default function Page() { - return ( - -
- - {(queryRef1) => ( - <> - - - - )} - - - {(queryRef2) => ( - loading}> - - - - )} - -
-
- ); -} diff --git a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/styles.css b/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/styles.css deleted file mode 100644 index 1aaf8dc2..00000000 --- a/integration-test/nextjs/src/app/rsc/dynamic/PreloadQuery/queryRef-refTest/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -.invalid { - border: 1px solid red; -} - -form { - display: flex; - flex-direction: column; -} diff --git a/integration-test/playwright/src/cc-dynamic.test.ts b/integration-test/playwright/src/cc-dynamic.test.ts index 95573206..3d460444 100644 --- a/integration-test/playwright/src/cc-dynamic.test.ts +++ b/integration-test/playwright/src/cc-dynamic.test.ts @@ -151,6 +151,9 @@ test.describe("CC dynamic", () => { } ); }); + + test.fixme("useSuspenseQuery with @defer", { tag: ["@tanstack"] }, () => {}); + test.describe("useSuspenseQuery with a nonce", () => { test( "invalid: logs an error", diff --git a/integration-test/playwright/src/preloadQuery.test.ts b/integration-test/playwright/src/preloadQuery.test.ts index ebed4ff0..67ec6224 100644 --- a/integration-test/playwright/src/preloadQuery.test.ts +++ b/integration-test/playwright/src/preloadQuery.test.ts @@ -7,19 +7,23 @@ const reactErr419 = /(Minified React error #419|Switched to client rendering)/; const base = matchesTag("@nextjs") ? "/rsc/dynamic/PreloadQuery" : "/preloadQuery"; -test.describe( - "PreloadQuery", - { - tag: ["@nextjs"], - }, - () => { - for (const [description, path] of [ - ["with useSuspenseQuery", "useSuspenseQuery"], - ["with queryRef and useReadQuery", "queryRef-useReadQuery"], - ] as const) { - test.describe(description, () => { - test("query resolves on the server", async ({ page, blockRequest }) => { - await page.goto(`${base}/${path}?errorIn=ssr,browser`, { + +const originatesIn = matchesTag("@nextjs") ? "RSC" : "SSR"; +const otherEnvs = matchesTag("@nextjs") ? "ssr,browser" : "browser"; + +test.describe("PreloadQuery", () => { + for (const [description, path] of [ + ["with useSuspenseQuery", "useSuspenseQuery"], + // ["with queryRef and useReadQuery", "queryRef-useReadQuery"], + ] as const) { + test.describe(description, () => { + test( + "query resolves on the server", + { + tag: ["@nextjs", "@tanstack"], + }, + async ({ page, blockRequest }) => { + await page.goto(`${base}/${path}?errorIn=${otherEnvs}`, { waitUntil: "commit", }); @@ -27,17 +31,24 @@ test.describe( await expect(page.getByText("loading")).not.toBeVisible(); await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible(); await expect( - page.getByText("Queried in RSC environment") + page.getByText(`Queried in ${originatesIn} environment`) ).toBeVisible(); - }); - - test("link chain errors on the server, restarts in the browser", async ({ - page, - }) => { + } + ); + + test( + "link chain errors on the server, restarts in the browser", + { + tag: ["@nextjs", "@tanstack"], + }, + async ({ page }) => { page.allowErrors?.(); - await page.goto(`${base}/${path}?errorIn=rsc,network_error`, { - waitUntil: "commit", - }); + await page.goto( + `${base}/${path}?errorIn=${originatesIn.toLowerCase()},network_error`, + { + waitUntil: "commit", + } + ); await expect(page).toBeInitiallyLoading(true); @@ -50,19 +61,26 @@ test.describe( await expect( page.getByText("Queried in Browser environment") ).toBeVisible(); - }); - - if (path === "queryRef-useReadQuery") { - // this only works for `useReadQuery`, because `useSuspenseQuery` won't attach - // to the exact same suspenseCache entry and as a result, it won't get the - // error message from the ReadableStream. - test("graphqlError on the server, transported to the browser, can be restarted", async ({ - page, - }) => { + } + ); + + if (path === "queryRef-useReadQuery") { + // this only works for `useReadQuery`, because `useSuspenseQuery` won't attach + // to the exact same suspenseCache entry and as a result, it won't get the + // error message from the ReadableStream. + test( + "graphqlError on the server, transported to the browser, can be restarted", + { + tag: ["@nextjs"], + }, + async ({ page }) => { page.allowErrors?.(); - await page.goto(`${base}/${path}?errorIn=rsc`, { - waitUntil: "commit", - }); + await page.goto( + `${base}/${path}?errorIn=${originatesIn.toLowerCase()}`, + { + waitUntil: "commit", + } + ); await expect(page).toBeInitiallyLoading(true); @@ -79,21 +97,28 @@ test.describe( await expect( page.getByText("Queried in Browser environment") ).toBeVisible(); - }); - } else { - // instead, `useSuspenseQuery` will behave as if nothing had been transported - // and rerun the query in the browser. - // there is a chance it will also rerun the query during SSR, that's a timing - // question that might need further investigation - // the bottom line: `PreloadQuery` with `useSuspenseQuery` works in the happy - // path, but it's not as robust as `queryRef` with `useReadQuery`. - test("graphqlError on the server, restarts in the browser", async ({ - page, - }) => { + } + ); + } else { + // instead, `useSuspenseQuery` will behave as if nothing had been transported + // and rerun the query in the browser. + // there is a chance it will also rerun the query during SSR, that's a timing + // question that might need further investigation + // the bottom line: `PreloadQuery` with `useSuspenseQuery` works in the happy + // path, but it's not as robust as `queryRef` with `useReadQuery`. + test( + "graphqlError on the server, restarts in the browser", + { + tag: ["@nextjs", "@tanstack"], + }, + async ({ page }) => { page.allowErrors?.(); - await page.goto(`${base}/${path}?errorIn=rsc`, { - waitUntil: "commit", - }); + await page.goto( + `${base}/${path}?errorIn=${originatesIn.toLowerCase()}`, + { + waitUntil: "commit", + } + ); await expect(page).toBeInitiallyLoading(true); @@ -108,12 +133,18 @@ test.describe( await expect( page.getByText("Queried in Browser environment") ).toBeVisible(); - }); - } - }); - } + } + ); + } + }); + } - test("queryRef works with useQueryRefHandlers", async ({ page }) => { + test( + "queryRef works with useQueryRefHandlers", + { + tag: ["@nextjs"], + }, + async ({ page }) => { await page.goto(`${base}/queryRef-useReadQuery`, { waitUntil: "commit", }); @@ -127,20 +158,6 @@ test.describe( await expect( page.getByText("Queried in Browser environment") ).toBeVisible(); - }); - - test.skip("queryRef: assumptions about referential equality", async ({ - page, - }) => { - await page.goto(`${base}/queryRef-refTest`, { - waitUntil: "commit", - }); - - await page.getByRole("spinbutton").nth(11).waitFor(); - - for (let i = 0; i < 12; i++) { - await expect(page.getByRole("spinbutton").nth(i)).toHaveClass("valid"); - } - }); - } -); + } + ); +}); diff --git a/integration-test/shared/errorLink.tsx b/integration-test/shared/errorLink.tsx index cd9a5e60..21afb259 100644 --- a/integration-test/shared/errorLink.tsx +++ b/integration-test/shared/errorLink.tsx @@ -40,6 +40,7 @@ export const errorLink = new ApolloLink((operation, forward) => { } satisfies GraphQLFormattedError as GraphQLError, ], }); + subscriber.complete(); } }); } diff --git a/integration-test/tanstack-start/app/routeTree.gen.ts b/integration-test/tanstack-start/app/routeTree.gen.ts index 739cfbad..9b899e83 100644 --- a/integration-test/tanstack-start/app/routeTree.gen.ts +++ b/integration-test/tanstack-start/app/routeTree.gen.ts @@ -19,6 +19,8 @@ import { Route as UseBackgroundQueryWithoutSsrReadQueryImport } from './routes/u import { Route as UseBackgroundQueryImport } from './routes/useBackgroundQuery' import { Route as LoaderDeferImport } from './routes/loader-defer' import { Route as IndexImport } from './routes/index' +import { Route as PreloadQueryUseSuspenseQueryImport } from './routes/preloadQuery/useSuspenseQuery' +import { Route as PreloadQueryQueryRefUseReadQueryImport } from './routes/preloadQuery/queryRef-useReadQuery' // Create/Update Routes @@ -71,6 +73,20 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const PreloadQueryUseSuspenseQueryRoute = + PreloadQueryUseSuspenseQueryImport.update({ + id: '/preloadQuery/useSuspenseQuery', + path: '/preloadQuery/useSuspenseQuery', + getParentRoute: () => rootRoute, + } as any) + +const PreloadQueryQueryRefUseReadQueryRoute = + PreloadQueryQueryRefUseReadQueryImport.update({ + id: '/preloadQuery/queryRef-useReadQuery', + path: '/preloadQuery/queryRef-useReadQuery', + getParentRoute: () => rootRoute, + } as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -131,6 +147,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof UseSuspenseQueryDeferImport parentRoute: typeof rootRoute } + '/preloadQuery/queryRef-useReadQuery': { + id: '/preloadQuery/queryRef-useReadQuery' + path: '/preloadQuery/queryRef-useReadQuery' + fullPath: '/preloadQuery/queryRef-useReadQuery' + preLoaderRoute: typeof PreloadQueryQueryRefUseReadQueryImport + parentRoute: typeof rootRoute + } + '/preloadQuery/useSuspenseQuery': { + id: '/preloadQuery/useSuspenseQuery' + path: '/preloadQuery/useSuspenseQuery' + fullPath: '/preloadQuery/useSuspenseQuery' + preLoaderRoute: typeof PreloadQueryUseSuspenseQueryImport + parentRoute: typeof rootRoute + } } } @@ -145,6 +175,8 @@ export interface FileRoutesByFullPath { '/useQueryWithCache': typeof UseQueryWithCacheRoute '/useSuspenseQuery': typeof UseSuspenseQueryRoute '/useSuspenseQuery-defer': typeof UseSuspenseQueryDeferRoute + '/preloadQuery/queryRef-useReadQuery': typeof PreloadQueryQueryRefUseReadQueryRoute + '/preloadQuery/useSuspenseQuery': typeof PreloadQueryUseSuspenseQueryRoute } export interface FileRoutesByTo { @@ -156,6 +188,8 @@ export interface FileRoutesByTo { '/useQueryWithCache': typeof UseQueryWithCacheRoute '/useSuspenseQuery': typeof UseSuspenseQueryRoute '/useSuspenseQuery-defer': typeof UseSuspenseQueryDeferRoute + '/preloadQuery/queryRef-useReadQuery': typeof PreloadQueryQueryRefUseReadQueryRoute + '/preloadQuery/useSuspenseQuery': typeof PreloadQueryUseSuspenseQueryRoute } export interface FileRoutesById { @@ -168,6 +202,8 @@ export interface FileRoutesById { '/useQueryWithCache': typeof UseQueryWithCacheRoute '/useSuspenseQuery': typeof UseSuspenseQueryRoute '/useSuspenseQuery-defer': typeof UseSuspenseQueryDeferRoute + '/preloadQuery/queryRef-useReadQuery': typeof PreloadQueryQueryRefUseReadQueryRoute + '/preloadQuery/useSuspenseQuery': typeof PreloadQueryUseSuspenseQueryRoute } export interface FileRouteTypes { @@ -181,6 +217,8 @@ export interface FileRouteTypes { | '/useQueryWithCache' | '/useSuspenseQuery' | '/useSuspenseQuery-defer' + | '/preloadQuery/queryRef-useReadQuery' + | '/preloadQuery/useSuspenseQuery' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -191,6 +229,8 @@ export interface FileRouteTypes { | '/useQueryWithCache' | '/useSuspenseQuery' | '/useSuspenseQuery-defer' + | '/preloadQuery/queryRef-useReadQuery' + | '/preloadQuery/useSuspenseQuery' id: | '__root__' | '/' @@ -201,6 +241,8 @@ export interface FileRouteTypes { | '/useQueryWithCache' | '/useSuspenseQuery' | '/useSuspenseQuery-defer' + | '/preloadQuery/queryRef-useReadQuery' + | '/preloadQuery/useSuspenseQuery' fileRoutesById: FileRoutesById } @@ -213,6 +255,8 @@ export interface RootRouteChildren { UseQueryWithCacheRoute: typeof UseQueryWithCacheRoute UseSuspenseQueryRoute: typeof UseSuspenseQueryRoute UseSuspenseQueryDeferRoute: typeof UseSuspenseQueryDeferRoute + PreloadQueryQueryRefUseReadQueryRoute: typeof PreloadQueryQueryRefUseReadQueryRoute + PreloadQueryUseSuspenseQueryRoute: typeof PreloadQueryUseSuspenseQueryRoute } const rootRouteChildren: RootRouteChildren = { @@ -225,6 +269,8 @@ const rootRouteChildren: RootRouteChildren = { UseQueryWithCacheRoute: UseQueryWithCacheRoute, UseSuspenseQueryRoute: UseSuspenseQueryRoute, UseSuspenseQueryDeferRoute: UseSuspenseQueryDeferRoute, + PreloadQueryQueryRefUseReadQueryRoute: PreloadQueryQueryRefUseReadQueryRoute, + PreloadQueryUseSuspenseQueryRoute: PreloadQueryUseSuspenseQueryRoute, } export const routeTree = rootRoute @@ -244,7 +290,9 @@ export const routeTree = rootRoute "/useQuery", "/useQueryWithCache", "/useSuspenseQuery", - "/useSuspenseQuery-defer" + "/useSuspenseQuery-defer", + "/preloadQuery/queryRef-useReadQuery", + "/preloadQuery/useSuspenseQuery" ] }, "/": { @@ -270,6 +318,12 @@ export const routeTree = rootRoute }, "/useSuspenseQuery-defer": { "filePath": "useSuspenseQuery-defer.tsx" + }, + "/preloadQuery/queryRef-useReadQuery": { + "filePath": "preloadQuery/queryRef-useReadQuery.tsx" + }, + "/preloadQuery/useSuspenseQuery": { + "filePath": "preloadQuery/useSuspenseQuery.tsx" } } } diff --git a/integration-test/tanstack-start/app/routes/__root.tsx b/integration-test/tanstack-start/app/routes/__root.tsx index 77c3841e..88c08451 100644 --- a/integration-test/tanstack-start/app/routes/__root.tsx +++ b/integration-test/tanstack-start/app/routes/__root.tsx @@ -63,6 +63,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) { activeProps={{ className: "font-bold", }} + search={{ errorLevel: undefined }} > useSuspenseQuery {" "} diff --git a/integration-test/tanstack-start/app/routes/preloadQuery/queryRef-useReadQuery.tsx b/integration-test/tanstack-start/app/routes/preloadQuery/queryRef-useReadQuery.tsx new file mode 100644 index 00000000..dd8a19fc --- /dev/null +++ b/integration-test/tanstack-start/app/routes/preloadQuery/queryRef-useReadQuery.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/preloadQuery/queryRef-useReadQuery')({ + component: RouteComponent, +}) + +function RouteComponent() { + return
Hello "/preloadQuery/queryRef-useReadQuery"!
+} diff --git a/integration-test/tanstack-start/app/routes/preloadQuery/useSuspenseQuery.tsx b/integration-test/tanstack-start/app/routes/preloadQuery/useSuspenseQuery.tsx new file mode 100644 index 00000000..d5da6f3c --- /dev/null +++ b/integration-test/tanstack-start/app/routes/preloadQuery/useSuspenseQuery.tsx @@ -0,0 +1,48 @@ +import { QUERY } from "@integration-test/shared/queries"; +import { useSuspenseQuery, type DefaultContext } from "@apollo/client/index.js"; +import "@integration-test/shared/errorLink"; +import { createFileRoute } from "@tanstack/react-router"; +import { Suspense } from "react"; + +export const Route = createFileRoute("/preloadQuery/useSuspenseQuery")({ + component: RouteComponent, + validateSearch: (search: Record) => { + return { + errorIn: search.errorIn as DefaultContext["error"], + }; + }, + loaderDeps: ({ search: { errorIn } }) => ({ errorIn }), + loader: async ({ context: { preloadQuery }, deps: { errorIn } }) => { + const queryRef = preloadQuery(QUERY, { + context: { delay: 1000, ...(errorIn ? { error: errorIn } : {}) }, + }); + return { + queryRef, + }; + }, +}); + +function RouteComponent() { + return ( + loading}> + + + ); +} + +export function Child() { + const { errorIn } = Route.useSearch(); + const { data } = useSuspenseQuery(QUERY, { + context: { delay: 1000, ...(errorIn ? { error: errorIn } : {}) }, + }); + return ( + <> +
    + {data.products.map(({ id, title }: any) => ( +
  • {title}
  • + ))} +
+

Queried in {data.env} environment

+ + ); +} diff --git a/integration-test/tanstack-start/app/routes/useSuspenseQuery.tsx b/integration-test/tanstack-start/app/routes/useSuspenseQuery.tsx index f224cb46..6a035023 100644 --- a/integration-test/tanstack-start/app/routes/useSuspenseQuery.tsx +++ b/integration-test/tanstack-start/app/routes/useSuspenseQuery.tsx @@ -2,14 +2,14 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorBoundary, FallbackProps } from "react-error-boundary"; import { QUERY } from "@integration-test/shared/queries"; -import { useSuspenseQuery } from "@apollo/client/index.js"; +import { DefaultContext, useSuspenseQuery } from "@apollo/client/index.js"; import { Suspense } from "react"; export const Route = createFileRoute("/useSuspenseQuery")({ component: RouteComponent, validateSearch: (search: Record) => { return { - errorLevel: search.errorLevel as "ssr" | "always" | undefined, + errorLevel: search.errorLevel as DefaultContext["error"], }; }, }); @@ -33,11 +33,7 @@ function FallbackComponent({ error, resetErrorBoundary }: FallbackProps) { ); } -function Component({ - errorLevel, -}: { - errorLevel: "ssr" | "always" | undefined; -}) { +function Component({ errorLevel }: { errorLevel: DefaultContext["error"] }) { const { data } = useSuspenseQuery(QUERY, { context: { delay: 1000, ...(errorLevel ? { error: errorLevel } : {}) }, }); diff --git a/integration-test/tanstack-start/tsconfig.json b/integration-test/tanstack-start/tsconfig.json index 28632ab5..49579526 100644 --- a/integration-test/tanstack-start/tsconfig.json +++ b/integration-test/tanstack-start/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "jsx": "react-jsx", - "moduleResolution": "Bundler", - "module": "ESNext", "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", "skipLibCheck": true, "strictNullChecks": true, },