diff --git a/examples/app-dir-experiments/app/ssr/ApolloWrapper.tsx b/examples/app-dir-experiments/app/ssr/ApolloWrapper.tsx
index cb52c1f9..725586ef 100644
--- a/examples/app-dir-experiments/app/ssr/ApolloWrapper.tsx
+++ b/examples/app-dir-experiments/app/ssr/ApolloWrapper.tsx
@@ -1,14 +1,10 @@
"use client";
-import {
- ApolloClient,
- ApolloLink,
- HttpLink,
- SuspenseCache,
-} from "@apollo/client";
+import { ApolloLink, HttpLink, SuspenseCache } from "@apollo/client";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
+ NextSSRApolloClient,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
import { setVerbosity } from "ts-invariant";
@@ -21,7 +17,7 @@ function makeClient() {
fetchOptions: { cache: "no-store" },
});
- return new ApolloClient({
+ return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
diff --git a/examples/app-dir-experiments/package.json b/examples/app-dir-experiments/package.json
index af4cbbbf..d9777b84 100644
--- a/examples/app-dir-experiments/package.json
+++ b/examples/app-dir-experiments/package.json
@@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
- "@apollo/client": ">=3.8.0-beta.2",
+ "@apollo/client": ">=3.8.0-beta.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/server": "^4.6.0",
"@as-integrations/next": "^1.3.0",
diff --git a/examples/hack-the-supergraph-ssr/app/ApolloWrapper.tsx b/examples/hack-the-supergraph-ssr/app/ApolloWrapper.tsx
index 19cbf80f..c914cb2e 100644
--- a/examples/hack-the-supergraph-ssr/app/ApolloWrapper.tsx
+++ b/examples/hack-the-supergraph-ssr/app/ApolloWrapper.tsx
@@ -1,15 +1,11 @@
"use client";
import React from "react";
-import {
- ApolloClient,
- ApolloLink,
- HttpLink,
- SuspenseCache,
-} from "@apollo/client";
+import { ApolloLink, HttpLink, SuspenseCache } from "@apollo/client";
import clientCookies from "js-cookie";
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
+ NextSSRApolloClient,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
@@ -75,7 +71,7 @@ export function ApolloWrapper({
])
: ApolloLink.from([delayLink, httpLink]);
- return new ApolloClient({
+ return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link,
});
diff --git a/examples/hack-the-supergraph-ssr/app/error.tsx b/examples/hack-the-supergraph-ssr/app/error.tsx
index 686d12fa..1b836d61 100644
--- a/examples/hack-the-supergraph-ssr/app/error.tsx
+++ b/examples/hack-the-supergraph-ssr/app/error.tsx
@@ -4,16 +4,14 @@ import React from "react";
import { Box, Heading, Text, VStack } from "@chakra-ui/react";
export const Error = ({
- children,
- code,
error,
-}: React.PropsWithChildren<{
- code?: string;
- error: { toString(): string };
-}>) => (
+ reset,
+}: {
+ error: Error;
+ reset: () => void;
+}) => (
- {code ?? "Unknown Error"}
Houston, something went wrong on our end
Please review the information below for more details.
@@ -25,10 +23,9 @@ export const Error = ({
borderRadius="8px"
borderColor="brand.light"
>
- {error.toString()}
+ Error: {error.message}
)}
- {children}
);
diff --git a/examples/hack-the-supergraph-ssr/package.json b/examples/hack-the-supergraph-ssr/package.json
index 80227a09..0c9326bc 100644
--- a/examples/hack-the-supergraph-ssr/package.json
+++ b/examples/hack-the-supergraph-ssr/package.json
@@ -10,7 +10,7 @@
"lint": "next lint"
},
"dependencies": {
- "@apollo/client": ">=3.8.0-beta.2",
+ "@apollo/client": ">=3.8.0-beta.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/space-kit": "^9.11.0",
"@chakra-ui/next-js": "^2.1.2",
diff --git a/examples/polls-demo/app/cc/apollo-wrapper.tsx b/examples/polls-demo/app/cc/apollo-wrapper.tsx
index ec3e398a..59fcb8c0 100644
--- a/examples/polls-demo/app/cc/apollo-wrapper.tsx
+++ b/examples/polls-demo/app/cc/apollo-wrapper.tsx
@@ -1,12 +1,8 @@
"use client";
+import { ApolloLink, HttpLink, SuspenseCache } from "@apollo/client";
import {
- ApolloClient,
- ApolloLink,
- HttpLink,
- SuspenseCache,
-} from "@apollo/client";
-import {
+ NextSSRApolloClient,
ApolloNextAppProvider,
NextSSRInMemoryCache,
SSRMultipartLink,
@@ -25,7 +21,7 @@ function makeClient() {
uri: "https://fragrant-shadow-9470.fly.dev/",
});
- return new ApolloClient({
+ return new NextSSRApolloClient({
cache: new NextSSRInMemoryCache(),
link:
typeof window === "undefined"
diff --git a/examples/polls-demo/app/cc/error.tsx b/examples/polls-demo/app/cc/error.tsx
index ba4be923..775d0917 100644
--- a/examples/polls-demo/app/cc/error.tsx
+++ b/examples/polls-demo/app/cc/error.tsx
@@ -1,5 +1,11 @@
"use client";
-export default function Error(error: { toString(): string }) {
- return
Error: {error.toString()}
;
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error;
+ reset: () => void;
+}) {
+ return Error: {error.message}
;
}
diff --git a/examples/polls-demo/app/cc/loading.tsx b/examples/polls-demo/app/cc/loading.tsx
deleted file mode 100644
index 999450c4..00000000
--- a/examples/polls-demo/app/cc/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { PollSkeleton } from "@/components/poll";
-
-export default function Loading() {
- return ;
-}
diff --git a/examples/polls-demo/app/cc/page.tsx b/examples/polls-demo/app/cc/page.tsx
index b295992c..6da0b3e7 100644
--- a/examples/polls-demo/app/cc/page.tsx
+++ b/examples/polls-demo/app/cc/page.tsx
@@ -1,7 +1,7 @@
-import { Poll } from "./poll-cc";
+import { PollWrapper } from "./poll-cc";
export const dynamic = "force-dynamic";
export default async function Home() {
- return ;
+ return ;
}
diff --git a/examples/polls-demo/app/cc/poll-cc.tsx b/examples/polls-demo/app/cc/poll-cc.tsx
index 7522b0c8..0d669475 100644
--- a/examples/polls-demo/app/cc/poll-cc.tsx
+++ b/examples/polls-demo/app/cc/poll-cc.tsx
@@ -1,7 +1,11 @@
"use client";
-
-import { useSuspenseQuery } from "@apollo/experimental-nextjs-app-support/ssr";
+import { Suspense } from "react";
+import {
+ useReadQuery,
+ useBackgroundQuery,
+} from "@apollo/experimental-nextjs-app-support/ssr";
import { useMutation } from "@apollo/client";
+import { QueryReference } from "@apollo/client/react/cache/QueryReference";
import { Poll as PollInner } from "@/components/poll";
import { useState, useCallback } from "react";
@@ -9,15 +13,24 @@ import { useState, useCallback } from "react";
import {
AnswerPollDocument,
GetPollDocument,
+ GetPollQuery,
} from "@/components/poll/documents.generated";
-export const Poll = () => {
- const [showResults, setShowResults] = useState(false);
-
- const { data } = useSuspenseQuery(GetPollDocument, {
+export const PollWrapper = () => {
+ const [queryRef] = useBackgroundQuery(GetPollDocument, {
variables: { id: "1", delay: 0 },
});
+ return (
+ Loading...>}>
+
+
+ );
+};
+
+const Poll = ({ queryRef }: { queryRef: QueryReference }) => {
+ const { data } = useReadQuery(queryRef);
+ const [showResults, setShowResults] = useState(false);
const [mutate, { loading: mutationLoading }] =
useMutation(AnswerPollDocument);
diff --git a/examples/polls-demo/package.json b/examples/polls-demo/package.json
index bdb8fe38..bef66cd3 100644
--- a/examples/polls-demo/package.json
+++ b/examples/polls-demo/package.json
@@ -12,7 +12,7 @@
"codegen": "graphql-codegen --config codegen.ts"
},
"dependencies": {
- "@apollo/client": ">=3.8.0-beta.2",
+ "@apollo/client": ">=3.8.0-beta.4",
"@apollo/experimental-nextjs-app-support": "workspace:^",
"@apollo/server": "^4.7.0",
"@as-integrations/next": "^1.3.0",
diff --git a/package/.eslintrc.js b/package/.eslintrc.js
index 505b533b..a66876e0 100644
--- a/package/.eslintrc.js
+++ b/package/.eslintrc.js
@@ -20,5 +20,11 @@ module.exports = {
rules: {
"@typescript-eslint/no-explicit-any": "off",
"react/prop-types": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ varsIgnorePattern: "^_",
+ },
+ ],
},
};
diff --git a/package/README.md b/package/README.md
index 47f06456..15c39e76 100644
--- a/package/README.md
+++ b/package/README.md
@@ -82,7 +82,6 @@ First, create a new file `app/ApolloWrapper.js`:
// ^ this file needs the "use client" pragma
import {
- ApolloClient,
ApolloLink,
HttpLink,
SuspenseCache,
@@ -90,6 +89,7 @@ import {
import {
ApolloNextAppProvider,
NextSSRInMemoryCache,
+ NextSSRApolloClient,
SSRMultipartLink,
} from "@apollo/experimental-nextjs-app-support/ssr";
@@ -103,7 +103,7 @@ function makeClient() {
fetchOptions: { cache: "no-store" },
});
- return new ApolloClient({
+ return new NextSSRApolloClient({
// use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
cache: new NextSSRInMemoryCache(),
link:
@@ -164,7 +164,12 @@ export default function RootLayout({
> ☝️ This will work even if your layout is a React Server Component and will also allow the children of the layout to be React Server Components.
> It just makes sure that all Client Components will have access to the same Apollo Client instance, shared through the `ApolloNextAppProvider`.
-Now you can use the hooks `useQuery`, `useSuspenseQuery`, `useFragment`, and `useApolloClient` from `"@apollo/experimental-nextjs-app-support/ssr"` in your Client components like you are used to.
+You can import the following Apollo Client hooks from `"@apollo/experimental-nextjs-app-support/ssr"` in your client components (make sure you are not importing these hooks from `@apollo/client` as this package wraps and re-exports them to support streaming SSR):
+- `useQuery`
+- `useSuspenseQuery`
+- `useBackgroundQuery`
+- `useReadQuery`
+- `useFragment`
If you want to make the most of the streaming SSR features offered by React & the Next.js App Router, consider using the [`useSuspenseQuery`](https://www.apollographql.com/docs/react/api/react/hooks-experimental/#using-usesuspensequery_experimental) and [`useFragment`](https://www.apollographql.com/docs/react/api/react/hooks-experimental/#using-usefragment_experimental) hooks.
diff --git a/package/package.json b/package/package.json
index 214cce12..afbbba18 100644
--- a/package/package.json
+++ b/package/package.json
@@ -44,7 +44,7 @@
},
"packageManager": "yarn@3.5.0",
"devDependencies": {
- "@apollo/client": ">=3.8.0-beta.2",
+ "@apollo/client": ">=3.8.0-beta.4",
"@total-typescript/shoehorn": "^0.1.0",
"@tsconfig/recommended": "^1.0.1",
"@typescript-eslint/eslint-plugin": "latest",
@@ -59,7 +59,7 @@
"vitest": "^0.30.1"
},
"peerDependencies": {
- "@apollo/client": ">=3.8.0-beta.2",
+ "@apollo/client": ">=3.8.0-beta.4",
"next": "^13.4.1",
"react": "^18"
},
diff --git a/package/src/ssr/ApolloRehydrateSymbols.tsx b/package/src/ssr/ApolloRehydrateSymbols.tsx
index 5a83c98e..fd91fe1c 100644
--- a/package/src/ssr/ApolloRehydrateSymbols.tsx
+++ b/package/src/ssr/ApolloRehydrateSymbols.tsx
@@ -1,5 +1,9 @@
import { SuperJSONResult } from "superjson/dist/types";
-import { RehydrationCache, ResultsCache } from "./types";
+import {
+ RehydrationCache,
+ ResultsCache,
+ BackgroundQueriesCache,
+} from "./types";
import type { DataTransport } from "./dataTransport";
declare global {
@@ -7,8 +11,12 @@ declare global {
[ApolloRehydrationCache]?: RehydrationCache;
[ApolloResultCache]?: ResultsCache;
[ApolloSSRDataTransport]?: DataTransport;
+ [ApolloBackgroundQueryTransport]?: BackgroundQueriesCache;
}
}
export const ApolloRehydrationCache = Symbol.for("ApolloRehydrationCache");
export const ApolloResultCache = Symbol.for("ApolloResultCache");
export const ApolloSSRDataTransport = Symbol.for("ApolloSSRDataTransport");
+export const ApolloBackgroundQueryTransport = Symbol.for(
+ "ApolloBackgroundQueryTransport"
+);
diff --git a/package/src/ssr/NextSSRApolloClient.tsx b/package/src/ssr/NextSSRApolloClient.tsx
new file mode 100644
index 00000000..1158b2c3
--- /dev/null
+++ b/package/src/ssr/NextSSRApolloClient.tsx
@@ -0,0 +1,170 @@
+import {
+ ApolloClient,
+ ApolloClientOptions,
+ OperationVariables,
+ WatchQueryOptions,
+ Observable,
+ FetchResult,
+ DocumentNode,
+} from "@apollo/client";
+import type { QueryManager } from "@apollo/client/core/QueryManager";
+import { print } from "graphql";
+import { canonicalStringify } from "@apollo/client/cache";
+import { RehydrationContextValue } from "./types";
+import { registerLateInitializingQueue } from "./lateInitializingQueue";
+import {
+ ApolloBackgroundQueryTransport,
+ ApolloResultCache,
+} from "./ApolloRehydrateSymbols";
+
+function getQueryManager(
+ client: ApolloClient
+): QueryManager {
+ return client["queryManager"];
+}
+
+export class NextSSRApolloClient<
+ TCacheShape
+> extends ApolloClient {
+ private rehydrationContext: Pick<
+ RehydrationContextValue,
+ "incomingBackgroundQueries"
+ > = {
+ incomingBackgroundQueries: [],
+ };
+
+ constructor(options: ApolloClientOptions) {
+ super(options);
+
+ this.registerWindowHook();
+ }
+
+ private resolveFakeQueries = new Map<
+ string,
+ [(result: FetchResult) => void, (reason: any) => void]
+ >();
+
+ private identifyUniqueQuery(options: {
+ query: DocumentNode;
+ variables?: unknown;
+ }) {
+ const transformedDocument = this.documentTransform.transformDocument(
+ options.query
+ );
+ const queryManager = getQueryManager(this);
+ // Calling `transformDocument` will add __typename but won't remove client
+ // directives, so we need to get the `serverQuery`.
+ const { serverQuery } = queryManager.getDocumentInfo(transformedDocument);
+
+ const canonicalVariables = canonicalStringify(options.variables);
+
+ const cacheKey = [serverQuery, canonicalVariables].toString();
+
+ return { query: serverQuery, cacheKey, varJson: canonicalVariables };
+ }
+
+ private registerWindowHook() {
+ if (typeof window !== "undefined") {
+ if (Array.isArray(window[ApolloBackgroundQueryTransport] || [])) {
+ registerLateInitializingQueue(
+ ApolloBackgroundQueryTransport,
+ (options) => {
+ const { query, varJson, cacheKey } =
+ this.identifyUniqueQuery(options);
+
+ if (!query) return;
+ const printedServerQuery = print(query);
+ const queryManager = getQueryManager(this);
+
+ const byVariables =
+ queryManager["inFlightLinkObservables"].get(printedServerQuery) ||
+ new Map();
+
+ queryManager["inFlightLinkObservables"].set(
+ printedServerQuery,
+ byVariables
+ );
+
+ if (!byVariables.has(varJson)) {
+ const promise = new Promise((resolve, reject) => {
+ this.resolveFakeQueries.set(cacheKey, [resolve, reject]);
+ });
+ const cleanupCancelFn = () =>
+ queryManager["fetchCancelFns"].delete(cacheKey);
+
+ byVariables.set(
+ varJson,
+ new Observable((observer) => {
+ promise
+ .then((result) => {
+ observer.next(result);
+ observer.complete();
+ })
+ .catch((err) => {
+ observer.error(err);
+ })
+ .finally(() => {
+ this.resolveFakeQueries.delete(cacheKey);
+ cleanupCancelFn();
+ });
+ })
+ );
+
+ queryManager["fetchCancelFns"].set(
+ cacheKey,
+ (reason: unknown) => {
+ cleanupCancelFn();
+ const [_, reject] =
+ this.resolveFakeQueries.get(cacheKey) ?? [];
+ if (reject) {
+ reject(reason);
+ }
+ }
+ );
+ }
+ }
+ );
+ }
+
+ if (Array.isArray(window[ApolloResultCache] || [])) {
+ registerLateInitializingQueue(ApolloResultCache, (data) => {
+ const { cacheKey } = this.identifyUniqueQuery(data);
+ const [resolve] = this.resolveFakeQueries.get(cacheKey) ?? [];
+
+ if (resolve) {
+ resolve({
+ data: data.result,
+ });
+ // In order to avoid a scenario where the promise resolves without
+ // a query subscribing to the promise, we immediately call
+ // `cache.write` here.
+ // For more information, see: https://github.com/apollographql/apollo-client-nextjs/pull/38/files/388813a16e2ac5c62408923a1face9ae9417d92a#r1229870523
+ this.cache.write(data);
+ }
+ });
+ }
+ }
+ }
+
+ watchQuery<
+ T = any,
+ TVariables extends OperationVariables = OperationVariables
+ >(options: WatchQueryOptions) {
+ if (typeof window == "undefined") {
+ this.rehydrationContext.incomingBackgroundQueries.push(options);
+ }
+ const result = super.watchQuery(options);
+ return result;
+ }
+
+ setRehydrationContext(rehydrationContext: RehydrationContextValue) {
+ if (
+ rehydrationContext.incomingBackgroundQueries !==
+ this.rehydrationContext.incomingBackgroundQueries
+ )
+ rehydrationContext.incomingBackgroundQueries.push(
+ ...this.rehydrationContext.incomingBackgroundQueries.splice(0)
+ );
+ this.rehydrationContext = rehydrationContext;
+ }
+}
diff --git a/package/src/ssr/NextSSRInMemoryCache.tsx b/package/src/ssr/NextSSRInMemoryCache.tsx
index ac4e2edd..9d505c77 100644
--- a/package/src/ssr/NextSSRInMemoryCache.tsx
+++ b/package/src/ssr/NextSSRInMemoryCache.tsx
@@ -4,9 +4,7 @@ import {
Cache,
Reference,
} from "@apollo/client";
-import { ApolloResultCache } from "./ApolloRehydrateSymbols";
import { RehydrationContextValue } from "./types";
-import { registerLateInitializingQueue } from "./lateInitializingQueue";
export class NextSSRInMemoryCache extends InMemoryCache {
private rehydrationContext: Pick<
@@ -18,21 +16,6 @@ export class NextSSRInMemoryCache extends InMemoryCache {
};
constructor(config?: InMemoryCacheConfig) {
super(config);
-
- this.registerWindowHook();
- }
- private registerWindowHook() {
- if (typeof window !== "undefined") {
- if (Array.isArray(window[ApolloResultCache] || [])) {
- registerLateInitializingQueue(ApolloResultCache, (data) =>
- this.write(data)
- );
- } else {
- throw new Error(
- "On the client side, only one instance of `NextSSRInMemoryCache` can be created!"
- );
- }
- }
}
write(options: Cache.WriteOptions): Reference | undefined {
diff --git a/package/src/ssr/RehydrationContext.tsx b/package/src/ssr/RehydrationContext.tsx
index a2f5d26f..0ad93f14 100644
--- a/package/src/ssr/RehydrationContext.tsx
+++ b/package/src/ssr/RehydrationContext.tsx
@@ -5,6 +5,7 @@ import { ServerInsertedHTMLContext } from "next/navigation";
import { RehydrationContextValue } from "./types";
import { registerDataTransport, transportDataToJS } from "./dataTransport";
import invariant from "ts-invariant";
+import { NextSSRApolloClient } from "./NextSSRApolloClient";
const ApolloRehydrationContext = React.createContext<
RehydrationContextValue | undefined
@@ -13,15 +14,21 @@ const ApolloRehydrationContext = React.createContext<
export const RehydrationContextProvider = ({
children,
}: React.PropsWithChildren) => {
- const { cache } = useApolloClient();
+ const client = useApolloClient();
const rehydrationContext = React.useRef();
if (typeof window == "undefined") {
if (!rehydrationContext.current) {
rehydrationContext.current = buildApolloRehydrationContext();
}
-
- if (cache instanceof NextSSRInMemoryCache) {
- cache.setRehydrationContext(rehydrationContext.current);
+ if (client instanceof NextSSRApolloClient) {
+ client.setRehydrationContext(rehydrationContext.current);
+ } else {
+ throw new Error(
+ "When using Next SSR, you must use the `NextSSRApolloClient`"
+ );
+ }
+ if (client.cache instanceof NextSSRInMemoryCache) {
+ client.cache.setRehydrationContext(rehydrationContext.current);
} else {
throw new Error(
"When using Next SSR, you must use the `NextSSRInMemoryCache`"
@@ -61,11 +68,13 @@ function buildApolloRehydrationContext(): RehydrationContextValue {
transportValueData: {},
transportedValues: {},
incomingResults: [],
+ incomingBackgroundQueries: [],
RehydrateOnClient() {
rehydrationContext.currentlyInjected = false;
if (
!Object.keys(rehydrationContext.transportValueData).length &&
- !Object.keys(rehydrationContext.incomingResults).length
+ !Object.keys(rehydrationContext.incomingResults).length &&
+ !Object.keys(rehydrationContext.incomingBackgroundQueries).length
)
return <>>;
invariant.debug(
@@ -76,6 +85,10 @@ function buildApolloRehydrationContext(): RehydrationContextValue {
"transporting results",
rehydrationContext.incomingResults
);
+ invariant.debug(
+ "transporting incomingBackgroundQueries",
+ rehydrationContext.incomingBackgroundQueries
+ );
const __html = transportDataToJS({
rehydrate: Object.fromEntries(
@@ -85,6 +98,7 @@ function buildApolloRehydrationContext(): RehydrationContextValue {
)
),
results: rehydrationContext.incomingResults,
+ backgroundQueries: rehydrationContext.incomingBackgroundQueries,
});
Object.assign(
rehydrationContext.transportedValues,
@@ -92,6 +106,7 @@ function buildApolloRehydrationContext(): RehydrationContextValue {
);
rehydrationContext.transportValueData = {};
rehydrationContext.incomingResults = [];
+ rehydrationContext.incomingBackgroundQueries = [];
return (