Skip to content
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

Honor @nonreactive with useFragment and cache.watchFragment #11844

Merged
merged 9 commits into from
May 14, 2024
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
Loading