diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b05e0936cd..288ab54ac97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,9 @@ - Avoid registering `QueryPromise` when `skip` is `true` during server-side rendering.
[@izumin5210](https://github.com/izumin5210) in [#7310](https://github.com/apollographql/apollo-client/pull/7310) +- Cancel `queryInfo.notifyTimeout` in `QueryInfo#markResult` to prevent unnecessary network requests when using a `FetchPolicy` of `cache-and-network` or `network-only` in a React component with multiple `useQuery` calls.
+ [@benjamn](https://github.com/benjamn) in [#7347](https://github.com/apollographql/apollo-client/pull/7347) + ## Apollo Client 3.2.7 ## Bug Fixes diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index e7d9ae74570..69ef5dea972 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -49,6 +49,13 @@ function wrapDestructiveCacheMethod( } } +function cancelNotifyTimeout(info: QueryInfo) { + if (info["notifyTimeout"]) { + clearTimeout(info["notifyTimeout"]); + info["notifyTimeout"] = void 0; + } +} + // A QueryInfo object represents a single query managed by the // QueryManager, which tracks all QueryInfo objects by queryId in its // this.queries Map. QueryInfo objects store the latest results and errors @@ -191,10 +198,7 @@ export class QueryInfo { } notify() { - if (this.notifyTimeout) { - clearTimeout(this.notifyTimeout); - this.notifyTimeout = void 0; - } + cancelNotifyTimeout(this); if (this.shouldNotify()) { this.listeners.forEach(listener => listener(this)); @@ -292,6 +296,11 @@ export class QueryInfo { ) { this.graphQLErrors = isNonEmptyArray(result.errors) ? result.errors : []; + // If there is a pending notify timeout, cancel it because we are + // about to update this.diff to hold the latest data, and we can + // assume the data will be broadcast through some other mechanism. + cancelNotifyTimeout(this); + if (options.fetchPolicy === 'no-cache') { this.diff = { result: result.data, complete: true }; @@ -399,6 +408,8 @@ export class QueryInfo { this.networkStatus = NetworkStatus.error; this.lastWrite = void 0; + cancelNotifyTimeout(this); + if (error.graphQLErrors) { this.graphQLErrors = error.graphQLErrors; } diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index ad80a1199de..c1c5faeffd7 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -3,7 +3,7 @@ import { DocumentNode, GraphQLError } from 'graphql'; import gql from 'graphql-tag'; import { render, cleanup, wait } from '@testing-library/react'; -import { ApolloClient, NetworkStatus, TypedDocumentNode } from '../../../core'; +import { ApolloClient, NetworkStatus, TypedDocumentNode, WatchQueryFetchPolicy } from '../../../core'; import { InMemoryCache } from '../../../cache'; import { ApolloProvider } from '../../context'; import { Observable, Reference, concatPagination } from '../../../utilities'; @@ -2439,4 +2439,154 @@ describe('useQuery Hook', () => { }).then(resolve, reject); }); }); + + describe("multiple useQuery calls per component", () => { + type ABFields = { + id: number; + name: string; + }; + + const aQuery: TypedDocumentNode<{ + a: ABFields; + }> = gql`query A { a { id name }}`; + + const bQuery: TypedDocumentNode<{ + b: ABFields; + }> = gql`query B { b { id name }}`; + + const aData = { + a: { + __typename: "A", + id: 65, + name: "ay", + }, + }; + + const bData = { + b: { + __typename: "B", + id: 66, + name: "bee", + }, + }; + + function makeClient() { + return new ApolloClient({ + cache: new InMemoryCache, + link: new ApolloLink(operation => new Observable(observer => { + switch (operation.operationName) { + case "A": + observer.next({ data: aData }); + break; + case "B": + observer.next({ data: bData }); + break; + } + observer.complete(); + })), + }); + } + + function check( + aFetchPolicy: WatchQueryFetchPolicy, + bFetchPolicy: WatchQueryFetchPolicy, + ) { + return ( + resolve: (result: any) => any, + reject: (reason: any) => any, + ) => { + let renderCount = 0; + + function App() { + const a = useQuery(aQuery, { + fetchPolicy: aFetchPolicy, + }); + + const b = useQuery(bQuery, { + fetchPolicy: bFetchPolicy, + }); + + switch (++renderCount) { + case 1: + expect(a.loading).toBe(true); + expect(b.loading).toBe(true); + expect(a.data).toBeUndefined(); + expect(b.data).toBeUndefined(); + break; + case 2: + expect(a.loading).toBe(false); + expect(b.loading).toBe(true); + expect(a.data).toEqual(aData); + expect(b.data).toBeUndefined(); + break; + case 3: + expect(a.loading).toBe(false); + expect(b.loading).toBe(false); + expect(a.data).toEqual(aData); + expect(b.data).toEqual(bData); + break; + default: + reject("too many renders: " + renderCount); + } + + return null; + } + + render( + + + + ); + + return wait(() => { + expect(renderCount).toBe(3); + }).then(resolve, reject); + }; + } + + itAsync("cache-first for both", check( + "cache-first", + "cache-first", + )); + + itAsync("cache-first first, cache-and-network second", check( + "cache-first", + "cache-and-network", + )); + + itAsync("cache-first first, network-only second", check( + "cache-first", + "network-only", + )); + + itAsync("cache-and-network for both", check( + "cache-and-network", + "cache-and-network", + )); + + itAsync("cache-and-network first, cache-first second", check( + "cache-and-network", + "cache-first", + )); + + itAsync("cache-and-network first, network-only second", check( + "cache-and-network", + "network-only", + )); + + itAsync("network-only for both", check( + "network-only", + "network-only", + )); + + itAsync("network-only first, cache-first second", check( + "network-only", + "cache-first", + )); + + itAsync("network-only first, cache-and-network second", check( + "network-only", + "cache-and-network", + )); + }); });