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

Avoid making network requests when skip is true #6752

Merged
merged 1 commit into from
Aug 1, 2020
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Apollo Client 3.1.2 (not yet released)

## Bug Fixes

- Avoid making network requests when `skip` is `true`. <br/>
[@hwillson](https://github.com/hwillson) in [#6752](https://github.com/apollographql/apollo-client/pull/6752)

## Apollo Client 3.1.1

## Bug Fixes
Expand Down
9 changes: 9 additions & 0 deletions src/react/data/QueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export class QueryData<TData, TVariables> extends OperationData {
}

private updateObservableQuery() {
if (this.getOptions().skip) return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, when the first option starts with skip: true, there is a problem that refetch is not ready and cannot be executed. How about changing the order as follows?

private updateObservableQuery() {
    // If we skipped initially, we may not have yet created the observable
    if (!this.currentObservable) {
      this.initializeObservableQuery();
      return;
    }

    if (this.getOptions().skip) return;

Copy link

@sirugh sirugh Aug 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that this isn't a bug, but isn't an initial state with skip: true what lazy query is for?

ie

const [runQuery, response] = useLazyQuery(myQuery);
useEffect(() => {
  if (!skip) {
    runQuery();
  }
}, [skip]);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since there is no clear guidance on this in the official documents, a lot of users will think and use just like me. It is not easy to think of 'skip' affecting 'refetch' without any explanation.

And I can do it as you explained, but it has already become a strangely shaped code. And useLazyQuery's refetch function returns void, so we need to add more useEffect to use the response. The return value of useQuery's refetch is a promise, so you can use data directly with async/await. It's confusing why it was designed this way, but not all of these relationships seem clear and consistent.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just had exactly the same issue as you @gtolarc and was also using useQuery to get refetch because useLazyQuery returns void instead of a promise.

I'd be in favor of init the observable as well, as this change is quite breaking for a lot of people I guess 😕


// If we skipped initially, we may not have yet created the observable
if (!this.currentObservable) {
this.initializeObservableQuery();
Expand Down Expand Up @@ -326,6 +328,13 @@ export class QueryData<TData, TVariables> extends OperationData {
// When skipping a query (ie. we're not querying for data but still want
// to render children), make sure the `data` is cleared out and
// `loading` is set to `false` (since we aren't loading anything).
//
// NOTE: We no longer think this is the correct behavior. Skipping should
// not automatically set `data` to `undefined`, but instead leave the
// previous data in place. In other words, skipping should not mandate
// that previously received data is all of a sudden removed. Unfortunately,
// changing this is breaking, so we'll have to wait until Apollo Client
// 4.0 to address this.
Comment on lines +332 to +337
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

if (options.skip) {
result = {
...result,
Expand Down
43 changes: 34 additions & 9 deletions src/react/hoc/__tests__/queries/skip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -596,11 +596,16 @@ describe('[queries] skip', () => {
ranQuery++;
return f ? f(o) : null;
}).concat(
mockSingleLink({
request: { query },
result: { data },
newData: () => ({ data: nextData })
})
mockSingleLink(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was behaving flakily due to its use of MockedProvider's newData function, which is kinda broken. I've adjusted the test to be more specific about the mocked requests it's working with, and added a few quick notes about the full workflow.

{
request: { query },
result: { data },
},
{
request: { query },
result: { data: nextData },
},
)
);

const client = new ApolloClient({
Expand All @@ -626,26 +631,46 @@ describe('[queries] skip', () => {
expect(ranQuery).toBe(1);
break;
case 2:
// The first batch of data is fetched over the network, and
// verified here, followed by telling the component we want to
// skip running subsequent queries.
expect(this.props.data.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(data.allPeople);
expect(ranQuery).toBe(1);
setTimeout(() => {
this.props.setSkip(true);
});
break;
case 3:
// This render is triggered after setting skip to true. Now
// let's set skip to false to re-trigger the query.
expect(this.props.data).toBeUndefined();
expect(ranQuery).toBe(1);
setTimeout(() => {
this.props.setSkip(false);
});
break;
case 4:
expect(this.props.data!.loading).toBe(true);
expect(ranQuery).toBe(3);
// Since the `nextFetchPolicy` was set to `cache-first`, our
// query isn't loading as it's able to find the result of the
// query directly from the cache. Let's trigger a refetch
// to manually load the next batch of data.
expect(this.props.data!.loading).toBe(false);
expect(this.props.data.allPeople).toEqual(data.allPeople);
expect(ranQuery).toBe(1);
setTimeout(() => {
this.props.data.refetch();
});
break;
case 5:
expect(this.props.data!.loading).toBe(true);
expect(ranQuery).toBe(2);
break;
case 6:
// The next batch of data has loaded.
expect(this.props.data!.loading).toBe(false);
expect(ranQuery).toBe(3);
expect(this.props.data.allPeople).toEqual(nextData.allPeople);
expect(ranQuery).toBe(2);
break;
default:
}
Expand Down Expand Up @@ -673,7 +698,7 @@ describe('[queries] skip', () => {
);

await wait(() => {
expect(count).toEqual(5);
expect(count).toEqual(6);
});
});

Expand Down
63 changes: 63 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2162,5 +2162,68 @@ describe('useQuery Hook', () => {
expect(renderCount).toBe(3);
}).then(resolve, reject);
});

itAsync('should not make network requests when `skip` is `true`', (resolve, reject) => {
let networkRequestCount = 0;
const link = new ApolloLink((o, f) => {
networkRequestCount += 1;
return f ? f(o) : null;
}).concat(mockSingleLink(
{
request: {
query: CAR_QUERY,
variables: { someVar: true }
},
result: { data: CAR_RESULT_DATA }
}
));

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

let renderCount = 0;
const Component = () => {
const [skip, setSkip] = useState(false);
const { loading, data } = useQuery(CAR_QUERY, {
fetchPolicy: 'no-cache',
skip,
variables: { someVar: !skip }
});

switch (++renderCount) {
case 1:
expect(loading).toBeTruthy();
expect(data).toBeUndefined();
break;
case 2:
expect(loading).toBeFalsy();
expect(data).toEqual(CAR_RESULT_DATA);
expect(networkRequestCount).toBe(1);
setTimeout(() => setSkip(true));
break;
case 3:
expect(loading).toBeFalsy();
expect(data).toBeUndefined();
expect(networkRequestCount).toBe(1);
break;
default:
reject('too many renders');
}

return null;
};

render(
<ApolloProvider client={client}>
<Component />
</ApolloProvider>
);

return wait(() => {
expect(renderCount).toBe(3);
}).then(resolve, reject);
});
});
});