Skip to content

Commit

Permalink
Honor @nonreactive with useFragment and cache.watchFragment (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
jerelmiller committed May 14, 2024
1 parent 8475346 commit 86984f2
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/kind-donkeys-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Honor the `@nonreactive` directive when using `cache.watchFragment` or the `useFragment` hook to avoid rerendering when using these directives.
20 changes: 5 additions & 15 deletions src/__tests__/ApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2363,9 +2363,8 @@ describe("ApolloClient", () => {
expect.any(Error)
);
});
// The @nonreactive directive can only be used on fields or fragment
// spreads in queries, and currently has no effect here
it.failing("does not support the @nonreactive directive", async () => {

it("supports the @nonreactive directive", async () => {
const cache = new InMemoryCache();
const client = new ApolloClient({
cache,
Expand Down Expand Up @@ -2416,18 +2415,9 @@ describe("ApolloClient", () => {
},
});

{
const result = await stream.takeNext();

expect(result).toEqual({
data: {
__typename: "Item",
id: 5,
text: "Item #5",
},
complete: true,
});
}
await expect(stream.takeNext()).rejects.toThrow(
new Error("Timeout waiting for next event")
);
});
});

Expand Down
16 changes: 12 additions & 4 deletions src/cache/core/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type {
OperationVariables,
TypedDocumentNode,
} from "../../core/types.js";
import { equal } from "@wry/equality";
import type { MissingTree } from "./types/common.js";
import { equalByQuery } from "../../core/equalByQuery.js";

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

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

const diffOptions: Cache.DiffOptions<TData, TVars> = {
returnPartialData: true,
id: typeof from === "string" ? from : this.identify(from),
query: this.getFragmentDoc(fragment, fragmentName),
query,
optimistic,
};

Expand All @@ -243,9 +244,16 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
return this.watch<TData, TVars>({
...diffOptions,
immediate: true,
query: this.getFragmentDoc(fragment, fragmentName),
callback(diff) {
if (equal(diff, latestDiff)) {
if (
// Always ensure we deliver the first result
latestDiff &&
equalByQuery(
query,
{ data: latestDiff?.result },
{ data: diff.result }
)
) {
return;
}

Expand Down
144 changes: 144 additions & 0 deletions src/react/hooks/__tests__/useFragment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1414,6 +1414,150 @@ describe("useFragment", () => {
await expect(ProfiledHook).not.toRerender();
});

it("does not rerender when fields with @nonreactive change", async () => {
type Post = {
__typename: "User";
id: number;
title: string;
updatedAt: string;
};

const client = new ApolloClient({
cache: new InMemoryCache(),
});

const fragment: TypedDocumentNode<Post> = gql`
fragment PostFragment on Post {
id
title
updatedAt @nonreactive
}
`;

client.writeFragment({
fragment,
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-01-01",
},
});

const ProfiledHook = profileHook(() =>
useFragment({ fragment, from: { __typename: "Post", id: 1 } })
);

render(<ProfiledHook />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});

{
const snapshot = await ProfiledHook.takeSnapshot();

expect(snapshot).toEqual({
complete: true,
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-01-01",
},
});
}

client.writeFragment({
fragment,
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-02-01",
},
});

await expect(ProfiledHook).not.toRerender();
});

it("does not rerender when fields with @nonreactive on nested fragment change", async () => {
type Post = {
__typename: "User";
id: number;
title: string;
updatedAt: string;
};

const client = new ApolloClient({
cache: new InMemoryCache(),
});

const fragment: TypedDocumentNode<Post> = gql`
fragment PostFragment on Post {
id
title
...PostFields @nonreactive
}
fragment PostFields on Post {
updatedAt
}
`;

client.writeFragment({
fragment,
fragmentName: "PostFragment",
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-01-01",
},
});

const ProfiledHook = profileHook(() =>
useFragment({
fragment,
fragmentName: "PostFragment",
from: { __typename: "Post", id: 1 },
})
);

render(<ProfiledHook />, {
wrapper: ({ children }) => (
<ApolloProvider client={client}>{children}</ApolloProvider>
),
});

{
const snapshot = await ProfiledHook.takeSnapshot();

expect(snapshot).toEqual({
complete: true,
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-01-01",
},
});
}

client.writeFragment({
fragment,
fragmentName: "PostFragment",
data: {
__typename: "Post",
id: 1,
title: "Blog post",
updatedAt: "2024-02-01",
},
});

await expect(ProfiledHook).not.toRerender();
});

describe("tests with incomplete data", () => {
let cache: InMemoryCache, wrapper: React.FunctionComponent;
const ItemFragment = gql`
Expand Down

0 comments on commit 86984f2

Please sign in to comment.