Skip to content

Commit 86984f2

Browse files
authored
Honor @nonreactive with useFragment and cache.watchFragment (#11844)
1 parent 8475346 commit 86984f2

File tree

4 files changed

+166
-19
lines changed

4 files changed

+166
-19
lines changed

.changeset/kind-donkeys-clap.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Honor the `@nonreactive` directive when using `cache.watchFragment` or the `useFragment` hook to avoid rerendering when using these directives.

src/__tests__/ApolloClient.ts

+5-15
Original file line numberDiff line numberDiff line change
@@ -2363,9 +2363,8 @@ describe("ApolloClient", () => {
23632363
expect.any(Error)
23642364
);
23652365
});
2366-
// The @nonreactive directive can only be used on fields or fragment
2367-
// spreads in queries, and currently has no effect here
2368-
it.failing("does not support the @nonreactive directive", async () => {
2366+
2367+
it("supports the @nonreactive directive", async () => {
23692368
const cache = new InMemoryCache();
23702369
const client = new ApolloClient({
23712370
cache,
@@ -2416,18 +2415,9 @@ describe("ApolloClient", () => {
24162415
},
24172416
});
24182417

2419-
{
2420-
const result = await stream.takeNext();
2421-
2422-
expect(result).toEqual({
2423-
data: {
2424-
__typename: "Item",
2425-
id: 5,
2426-
text: "Item #5",
2427-
},
2428-
complete: true,
2429-
});
2430-
}
2418+
await expect(stream.takeNext()).rejects.toThrow(
2419+
new Error("Timeout waiting for next event")
2420+
);
24312421
});
24322422
});
24332423

src/cache/core/cache.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import type {
2121
OperationVariables,
2222
TypedDocumentNode,
2323
} from "../../core/types.js";
24-
import { equal } from "@wry/equality";
2524
import type { MissingTree } from "./types/common.js";
25+
import { equalByQuery } from "../../core/equalByQuery.js";
2626

2727
export type Transaction<T> = (c: ApolloCache<T>) => void;
2828

@@ -229,11 +229,12 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
229229
options: WatchFragmentOptions<TData, TVars>
230230
): Observable<WatchFragmentResult<TData>> {
231231
const { fragment, fragmentName, from, optimistic = true } = options;
232+
const query = this.getFragmentDoc(fragment, fragmentName);
232233

233234
const diffOptions: Cache.DiffOptions<TData, TVars> = {
234235
returnPartialData: true,
235236
id: typeof from === "string" ? from : this.identify(from),
236-
query: this.getFragmentDoc(fragment, fragmentName),
237+
query,
237238
optimistic,
238239
};
239240

@@ -243,9 +244,16 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
243244
return this.watch<TData, TVars>({
244245
...diffOptions,
245246
immediate: true,
246-
query: this.getFragmentDoc(fragment, fragmentName),
247247
callback(diff) {
248-
if (equal(diff, latestDiff)) {
248+
if (
249+
// Always ensure we deliver the first result
250+
latestDiff &&
251+
equalByQuery(
252+
query,
253+
{ data: latestDiff?.result },
254+
{ data: diff.result }
255+
)
256+
) {
249257
return;
250258
}
251259

src/react/hooks/__tests__/useFragment.test.tsx

+144
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,150 @@ describe("useFragment", () => {
14141414
await expect(ProfiledHook).not.toRerender();
14151415
});
14161416

1417+
it("does not rerender when fields with @nonreactive change", async () => {
1418+
type Post = {
1419+
__typename: "User";
1420+
id: number;
1421+
title: string;
1422+
updatedAt: string;
1423+
};
1424+
1425+
const client = new ApolloClient({
1426+
cache: new InMemoryCache(),
1427+
});
1428+
1429+
const fragment: TypedDocumentNode<Post> = gql`
1430+
fragment PostFragment on Post {
1431+
id
1432+
title
1433+
updatedAt @nonreactive
1434+
}
1435+
`;
1436+
1437+
client.writeFragment({
1438+
fragment,
1439+
data: {
1440+
__typename: "Post",
1441+
id: 1,
1442+
title: "Blog post",
1443+
updatedAt: "2024-01-01",
1444+
},
1445+
});
1446+
1447+
const ProfiledHook = profileHook(() =>
1448+
useFragment({ fragment, from: { __typename: "Post", id: 1 } })
1449+
);
1450+
1451+
render(<ProfiledHook />, {
1452+
wrapper: ({ children }) => (
1453+
<ApolloProvider client={client}>{children}</ApolloProvider>
1454+
),
1455+
});
1456+
1457+
{
1458+
const snapshot = await ProfiledHook.takeSnapshot();
1459+
1460+
expect(snapshot).toEqual({
1461+
complete: true,
1462+
data: {
1463+
__typename: "Post",
1464+
id: 1,
1465+
title: "Blog post",
1466+
updatedAt: "2024-01-01",
1467+
},
1468+
});
1469+
}
1470+
1471+
client.writeFragment({
1472+
fragment,
1473+
data: {
1474+
__typename: "Post",
1475+
id: 1,
1476+
title: "Blog post",
1477+
updatedAt: "2024-02-01",
1478+
},
1479+
});
1480+
1481+
await expect(ProfiledHook).not.toRerender();
1482+
});
1483+
1484+
it("does not rerender when fields with @nonreactive on nested fragment change", async () => {
1485+
type Post = {
1486+
__typename: "User";
1487+
id: number;
1488+
title: string;
1489+
updatedAt: string;
1490+
};
1491+
1492+
const client = new ApolloClient({
1493+
cache: new InMemoryCache(),
1494+
});
1495+
1496+
const fragment: TypedDocumentNode<Post> = gql`
1497+
fragment PostFragment on Post {
1498+
id
1499+
title
1500+
...PostFields @nonreactive
1501+
}
1502+
1503+
fragment PostFields on Post {
1504+
updatedAt
1505+
}
1506+
`;
1507+
1508+
client.writeFragment({
1509+
fragment,
1510+
fragmentName: "PostFragment",
1511+
data: {
1512+
__typename: "Post",
1513+
id: 1,
1514+
title: "Blog post",
1515+
updatedAt: "2024-01-01",
1516+
},
1517+
});
1518+
1519+
const ProfiledHook = profileHook(() =>
1520+
useFragment({
1521+
fragment,
1522+
fragmentName: "PostFragment",
1523+
from: { __typename: "Post", id: 1 },
1524+
})
1525+
);
1526+
1527+
render(<ProfiledHook />, {
1528+
wrapper: ({ children }) => (
1529+
<ApolloProvider client={client}>{children}</ApolloProvider>
1530+
),
1531+
});
1532+
1533+
{
1534+
const snapshot = await ProfiledHook.takeSnapshot();
1535+
1536+
expect(snapshot).toEqual({
1537+
complete: true,
1538+
data: {
1539+
__typename: "Post",
1540+
id: 1,
1541+
title: "Blog post",
1542+
updatedAt: "2024-01-01",
1543+
},
1544+
});
1545+
}
1546+
1547+
client.writeFragment({
1548+
fragment,
1549+
fragmentName: "PostFragment",
1550+
data: {
1551+
__typename: "Post",
1552+
id: 1,
1553+
title: "Blog post",
1554+
updatedAt: "2024-02-01",
1555+
},
1556+
});
1557+
1558+
await expect(ProfiledHook).not.toRerender();
1559+
});
1560+
14171561
describe("tests with incomplete data", () => {
14181562
let cache: InMemoryCache, wrapper: React.FunctionComponent;
14191563
const ItemFragment = gql`

0 commit comments

Comments
 (0)