-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RSC preloading mechanism #258
Changes from 11 commits
c6119dd
08ba665
047482c
a47cac9
e8057fc
293e03e
50642e4
fd7a7ed
558e52c
8600cdc
3a014a4
106e0c3
8ac604e
eb0e41b
750eecb
f9ea406
e583479
4051ae7
6c54609
f566049
4e05fcb
4f65c0f
66dffb5
4eb91c7
54f3569
5da45f9
8f3e580
40d0651
eb063ab
0916027
4615c72
b0339ce
1548891
8591ba7
902a598
c8a2ad5
15311cf
00585ea
0cc5dec
22b2924
56c0f3f
2177834
7cb215c
3413d9b
ab600e5
0f59a31
e10a769
3af3000
572f209
902f673
ba35077
2d454ff
e64236d
e08acd9
1e97538
b162c52
4735d7a
50e1a7c
799a6a1
74c2fd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { expect } from "@playwright/test"; | ||
import { test } from "../../../../../fixture"; | ||
|
||
test.describe("PreloadQuery", () => { | ||
for (const [decription, path] of [ | ||
["with useSuspenseQuery", "useSuspenseQuery"], | ||
["with queryRef and useReadQuery", "queryRef-useReadQuery"], | ||
] as const) { | ||
test.describe(decription, () => { | ||
test("query resolves on the server", async ({ page, blockRequest }) => { | ||
await page.goto( | ||
`http://localhost:3000/rsc/dynamic/PreloadQuery/${path}?errorIn=ssr,browser`, | ||
{ | ||
waitUntil: "commit", | ||
} | ||
); | ||
|
||
await expect(page).toBeInitiallyLoading(true); | ||
await expect(page.getByText("loading")).not.toBeVisible(); | ||
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible(); | ||
await expect( | ||
page.getByText("Queried in RSC environment") | ||
).toBeVisible(); | ||
}); | ||
|
||
test("query errors on the server, restarts in the browser", async ({ | ||
page, | ||
}) => { | ||
page.allowErrors?.(); | ||
await page.goto( | ||
`http://localhost:3000/rsc/dynamic/PreloadQuery/${path}?errorIn=rsc`, | ||
{ | ||
waitUntil: "commit", | ||
} | ||
); | ||
|
||
await expect(page).toBeInitiallyLoading(true); | ||
|
||
await page.waitForEvent("pageerror", (error) => { | ||
return ( | ||
/* prod */ error.message.includes("Minified React error #419") || | ||
/* dev */ error.message.includes("Query failed upstream.") | ||
); | ||
}); | ||
|
||
await expect(page.getByText("loading")).not.toBeVisible(); | ||
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible(); | ||
await expect( | ||
page.getByText("Queried in Browser environment") | ||
).toBeVisible(); | ||
}); | ||
}); | ||
} | ||
test("queryRef works with useQueryRefHandlers", async ({ page }) => { | ||
await page.goto( | ||
`http://localhost:3000/rsc/dynamic/PreloadQuery/queryRef-useReadQuery`, | ||
{ | ||
waitUntil: "commit", | ||
} | ||
); | ||
|
||
await expect(page).toBeInitiallyLoading(true); | ||
await expect(page.getByText("loading")).not.toBeVisible(); | ||
await expect(page.getByText("Soft Warm Apollo Beanie")).toBeVisible(); | ||
await expect(page.getByText("Queried in RSC environment")).toBeVisible(); | ||
|
||
await page.getByRole("button", { name: "refetch" }).click(); | ||
await expect( | ||
page.getByText("Queried in Browser environment") | ||
).toBeVisible(); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"use client"; | ||
|
||
import { | ||
QueryReference, | ||
useQueryRefHandlers, | ||
useReadQuery, | ||
} from "@apollo/client"; | ||
import { DynamicProductResult } from "../shared"; | ||
|
||
export function ClientChild({ | ||
queryRef, | ||
}: { | ||
queryRef: QueryReference<DynamicProductResult>; | ||
}) { | ||
const { data } = useReadQuery(queryRef); | ||
const { refetch } = useQueryRefHandlers(queryRef); | ||
return ( | ||
<> | ||
<ul> | ||
{data.products.map(({ id, title }: any) => ( | ||
<li key={id}>{title}</li> | ||
))} | ||
</ul> | ||
<p>Queried in {data.env} environment</p> | ||
<button onClick={() => refetch()}>refetch</button> | ||
phryneas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { ApolloWrapper } from "@/app/cc/ApolloWrapper"; | ||
import { PreloadQuery } from "@apollo/client-react-streaming"; | ||
import { ClientChild } from "./ClientChild"; | ||
import { QUERY } from "../shared"; | ||
|
||
export const dynamic = "force-dynamic"; | ||
import { getClient } from "../../../client"; | ||
import { Suspense } from "react"; | ||
|
||
export default function Page({ searchParams }: { searchParams?: any }) { | ||
return ( | ||
<ApolloWrapper> | ||
<PreloadQuery | ||
options={{ | ||
query: QUERY, | ||
context: { | ||
delay: 1000, | ||
error: searchParams?.errorIn || undefined, | ||
}, | ||
}} | ||
getClient={getClient} | ||
> | ||
{(queryRef) => ( | ||
<Suspense fallback={<>loading</>}> | ||
<ClientChild queryRef={queryRef as any /*TODO*/} /> | ||
</Suspense> | ||
)} | ||
</PreloadQuery> | ||
</ApolloWrapper> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { TypedDocumentNode, gql } from "@apollo/client"; | ||
|
||
export interface DynamicProductResult { | ||
products: { | ||
id: string; | ||
title: string; | ||
}[]; | ||
env: string; | ||
} | ||
export const QUERY: TypedDocumentNode<DynamicProductResult> = gql` | ||
query dynamicProducts { | ||
products { | ||
id | ||
title | ||
} | ||
env | ||
} | ||
`; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
"use client"; | ||
|
||
import { useSuspenseQuery } from "@apollo/client"; | ||
import { QUERY } from "../shared"; | ||
|
||
export function ClientChild() { | ||
const { data } = useSuspenseQuery(QUERY); | ||
return ( | ||
<> | ||
<ul> | ||
{data.products.map(({ id, title }: any) => ( | ||
<li key={id}>{title}</li> | ||
))} | ||
</ul> | ||
<p>Queried in {data.env} environment</p> | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { ApolloWrapper } from "@/app/cc/ApolloWrapper"; | ||
import { PreloadQuery } from "@apollo/client-react-streaming"; | ||
import { ClientChild } from "./ClientChild"; | ||
import { QUERY } from "../shared"; | ||
|
||
export const dynamic = "force-dynamic"; | ||
import { getClient } from "../../../client"; | ||
import { Suspense } from "react"; | ||
|
||
export default function Page({ searchParams }: { searchParams?: any }) { | ||
return ( | ||
<ApolloWrapper> | ||
<PreloadQuery | ||
options={{ | ||
query: QUERY, | ||
context: { | ||
delay: 1000, | ||
error: searchParams?.errorIn || undefined, | ||
}, | ||
}} | ||
getClient={getClient} | ||
> | ||
<Suspense fallback={<>loading</>}> | ||
<ClientChild /> | ||
</Suspense> | ||
</PreloadQuery> | ||
</ApolloWrapper> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,11 @@ | ||
import { ApolloLink, Observable } from "@apollo/client"; | ||
|
||
declare module "@apollo/client" { | ||
export interface DefaultContext { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooooo this is really something we should add to our docs. I don't think we recommend this at all and I'm sure lots of users would really appreciate being able to do this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. |
||
delay?: number; | ||
} | ||
} | ||
|
||
export const delayLink = new ApolloLink((operation, forward) => { | ||
if (operation.operationName?.includes("dynamic")) { | ||
operation.setContext({ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { ApolloLink, Observable } from "@apollo/client"; | ||
import { GraphQLError } from "graphql"; | ||
import * as entryPoint from "@apollo/client-react-streaming"; | ||
|
||
declare module "@apollo/client" { | ||
type Env = "ssr" | "browser" | "rsc"; | ||
export interface DefaultContext { | ||
error?: "always" | Env | `${Env},${Env}`; | ||
} | ||
} | ||
|
||
export const errorLink = new ApolloLink((operation, forward) => { | ||
const context = operation.getContext(); | ||
if ( | ||
context.error === "always" || | ||
("built_for_ssr" in entryPoint && | ||
context.error?.split(",").includes("ssr")) || | ||
("built_for_browser" in entryPoint && | ||
context.error?.split(",").includes("browser")) || | ||
("built_for_rsc" in entryPoint && context.error?.split(",").includes("rsc")) | ||
) { | ||
return new Observable((subscriber) => { | ||
subscriber.next({ | ||
data: null, | ||
errors: [new GraphQLError("Simulated error")], | ||
}); | ||
}); | ||
} | ||
return forward(operation); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just an additional guard to ensure this test never makes a query in the browser - not necessary but with the
error
context flag we now can do it :)