Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-horses-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": minor
---

SSR functions take batchOptions, to modulate query batching
140 changes: 140 additions & 0 deletions src/react/ssr/__tests__/getDataFromTree.batch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/** @jest-environment node */
import React from "react";
import { getDataFromTree, renderToStringWithData } from "../index.js";
import { ApolloClient, ApolloLink } from "@apollo/client";
import { InMemoryCache as Cache } from "@apollo/client/cache";
import { ApolloProvider, useQuery } from "@apollo/client/react";
import { gql } from "graphql-tag";
import { Observable } from "@apollo/client/utilities";

interface TestData {
test: string;
}

const TEST_QUERY = gql`
query TestQuery {
test
}
`;

const TestComponent = () => {
const { data, loading } = useQuery<TestData>(TEST_QUERY);

if (loading) return <div>Loading...</div>;
return <div>Data: {data?.test}</div>;
};

describe("getDataFromTree with batch options", () => {
let client: ApolloClient;

beforeEach(() => {
const mockLink = new ApolloLink(() => {
return new Observable((observer) => {
// Resolve immediately for testing
observer.next({ data: { test: "success" } });
observer.complete();
});
});

client = new ApolloClient({
cache: new Cache(),
link: mockLink,
});
});

it("should work with debounced batching", async () => {
const element = (
<ApolloProvider client={client}>
<TestComponent />
</ApolloProvider>
);

const startTime = Date.now();

const view = await getDataFromTree(
element,
{},
{
debounce: 50,
}
);

const endTime = Date.now();
const duration = endTime - startTime;

// Should complete within a reasonable time
expect(duration).toBeLessThan(200);
expect(view).toMatch(/Data:.*success/);
});

it("should work with renderToStringWithData and batching", async () => {
const element = (
<ApolloProvider client={client}>
<TestComponent />
</ApolloProvider>
);

const startTime = Date.now();

const view = await renderToStringWithData(element, {
debounce: 30,
});

const endTime = Date.now();
const duration = endTime - startTime;

// Should complete within a reasonable time
expect(duration).toBeLessThan(150);
expect(view).toMatch(/Data:.*success/);
});

it("should handle multiple queries with debouncing", async () => {
const MultiQueryComponent = () => {
const { data: data1 } = useQuery<TestData>(TEST_QUERY);
const { data: data2 } = useQuery<TestData>(TEST_QUERY);

return (
<div>
<div>Query 1: {data1?.test}</div>
<div>Query 2: {data2?.test}</div>
</div>
);
};

const element = (
<ApolloProvider client={client}>
<MultiQueryComponent />
</ApolloProvider>
);

const startTime = Date.now();

const view = await getDataFromTree(
element,
{},
{
debounce: 20,
}
);

const endTime = Date.now();
const duration = endTime - startTime;

// Should resolve quickly due to debouncing
expect(duration).toBeLessThan(100);
expect(view).toMatch(/Query 1:.*success/);
expect(view).toMatch(/Query 2:.*success/);
});

it("should fall back to default behavior when no batch options provided", async () => {
const element = (
<ApolloProvider client={client}>
<TestComponent />
</ApolloProvider>
);

const view = await getDataFromTree(element);

expect(view).toMatch(/Data:.*success/);
});
});
171 changes: 171 additions & 0 deletions src/react/ssr/__tests__/prerenderStatic.batch.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/** @jest-environment node */
import React from "react";
import { prerenderStatic } from "../prerenderStatic.js";
import { ApolloClient, ApolloLink } from "@apollo/client";
import { InMemoryCache as Cache } from "@apollo/client/cache";
import { ApolloProvider, useQuery } from "@apollo/client/react";
import { gql } from "graphql-tag";
import { Observable } from "@apollo/client/utilities";

interface TestData {
test: string;
}

const TEST_QUERY = gql`
query TestQuery {
test
}
`;

const TestComponent = () => {
const { data, loading } = useQuery<TestData>(TEST_QUERY);

if (loading) return <div>Loading...</div>;
return <div>Data: {data?.test}</div>;
};

describe("prerenderStatic with batch options", () => {
let client: ApolloClient;

beforeEach(() => {
const mockLink = new ApolloLink(() => {
return new Observable((observer) => {
// Resolve immediately for testing
observer.next({ data: { test: "success" } });
observer.complete();
});
});

client = new ApolloClient({
cache: new Cache(),
link: mockLink,
});
});

it("should work with debounced batching", async () => {
const element = (
<ApolloProvider client={client}>
<TestComponent />
</ApolloProvider>
);

const startTime = Date.now();

const result = await prerenderStatic({
tree: element,
renderFunction: (await import("react-dom/server")).renderToString,
batchOptions: {
debounce: 50,
},
});

const endTime = Date.now();
const duration = endTime - startTime;

// Should complete within a reasonable time
expect(duration).toBeLessThan(200);
expect(result.result).toMatch(/Data:.*success/);
});

it("should handle multiple queries with debouncing", async () => {
const MultiQueryComponent = () => {
const { data: data1 } = useQuery<TestData>(TEST_QUERY);
const { data: data2 } = useQuery<TestData>(TEST_QUERY);

return (
<div>
<div>Query 1: {data1?.test}</div>
<div>Query 2: {data2?.test}</div>
</div>
);
};

const element = (
<ApolloProvider client={client}>
<MultiQueryComponent />
</ApolloProvider>
);

const startTime = Date.now();

const result = await prerenderStatic({
tree: element,
renderFunction: (await import("react-dom/server")).renderToString,
batchOptions: {
debounce: 20,
},
});

const endTime = Date.now();
const duration = endTime - startTime;

// Should resolve quickly due to debouncing
expect(duration).toBeLessThan(100);
expect(result.result).toMatch(/Query 1:.*success/);
expect(result.result).toMatch(/Query 2:.*success/);
});

it("should fall back to default behavior when no batch options provided", async () => {
const element = (
<ApolloProvider client={client}>
<TestComponent />
</ApolloProvider>
);

const result = await prerenderStatic({
tree: element,
renderFunction: (await import("react-dom/server")).renderToString,
});

expect(result.result).toMatch(/Data:.*success/);
});

it("should handle slow queries with debouncing", async () => {
let resolveSlowQuery: (() => void) | null = null;
const slowPromise = new Promise<void>((resolve) => {
resolveSlowQuery = resolve;
});

const slowLink = new ApolloLink(() => {
return new Observable((observer) => {
// Delay the response
setTimeout(() => {
observer.next({ data: { test: "slow" } });
observer.complete();
if (resolveSlowQuery) resolveSlowQuery();
}, 100);
});
});

const slowClient = new ApolloClient({
cache: new Cache(),
link: slowLink,
});

const element = (
<ApolloProvider client={slowClient}>
<TestComponent />
</ApolloProvider>
);

const startTime = Date.now();

const result = await prerenderStatic({
tree: element,
renderFunction: (await import("react-dom/server")).renderToString,
batchOptions: {
debounce: 30,
},
});

const endTime = Date.now();
const duration = endTime - startTime;

// Should timeout around 30ms, not wait for the 100ms query
expect(duration).toBeGreaterThanOrEqual(25);
expect(duration).toBeLessThan(80);

// Wait for the slow query to complete
await slowPromise;
});
});
8 changes: 7 additions & 1 deletion src/react/ssr/getDataFromTree.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type * as ReactTypes from "react";

import { prerenderStatic } from "./prerenderStatic.js";
import type { BatchOptions } from "./types.js";

/**
* @deprecated This function uses the legacy `renderToStaticMarkup` API from React.
Expand All @@ -9,14 +10,16 @@ import { prerenderStatic } from "./prerenderStatic.js";
*/
export async function getDataFromTree(
tree: ReactTypes.ReactNode,
context: { [key: string]: any } = {}
context: { [key: string]: any } = {},
batchOptions?: BatchOptions
) {
return getMarkupFromTree({
tree,
context,
// If you need to configure this renderFunction, call getMarkupFromTree
// directly instead of getDataFromTree.
renderFunction: (await import("react-dom/server")).renderToStaticMarkup,
batchOptions,
});
}

Expand All @@ -26,6 +29,7 @@ type GetMarkupFromTreeOptions = {
renderFunction?:
| prerenderStatic.RenderToString
| prerenderStatic.RenderToStringPromise;
batchOptions?: BatchOptions;
};

/**
Expand All @@ -40,6 +44,7 @@ export async function getMarkupFromTree({
// the default, because it's a little less expensive than renderToString,
// and legacy usage of getDataFromTree ignores the return value anyway.
renderFunction,
batchOptions,
}: GetMarkupFromTreeOptions): Promise<string> {
if (!renderFunction) {
renderFunction = (await import("react-dom/server")).renderToStaticMarkup;
Expand All @@ -49,6 +54,7 @@ export async function getMarkupFromTree({
context,
renderFunction,
maxRerenders: Number.POSITIVE_INFINITY,
batchOptions,
});
return result;
}
Loading