Skip to content

Commit

Permalink
Ignore option callbacks when deciding to update a query
Browse files Browse the repository at this point in the history
`QueryData` stores last used options to help decide when it should
re-run. If new options (when compared against the previously
stored last options) are found, `QueryData` will make sure the
new options are passed into Apollo Client for processing.
When `onCompleted` and/or `onError` options are set however,
`QueryData` thinks the options received on each render are new
as these callback functions don't have a stable identity. This
can then lead to infinite re-renders.

This commit adjusts the `QueryData` option equality check to
ignore option callbacks. During normal use of `useQuery` it
should be okay to ignore callbacks like this, as they don't
normally change between renders.

Fixes #6301
  • Loading branch information
hwillson authored and benjamn committed Jul 16, 2020
1 parent f5eac73 commit d7dedc9
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 6 deletions.
17 changes: 11 additions & 6 deletions src/react/data/QueryData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
} from '../types/types';
import { OperationData } from './OperationData';

const stripFns = ({ onCompleted, onError, ...rest }: any = {}) => rest;

export class QueryData<TData, TVariables> extends OperationData {
public onNewData: () => void;

Expand Down Expand Up @@ -239,12 +241,15 @@ export class QueryData<TData, TVariables> extends OperationData {
children: null
};

if (
!equal(
newObservableQueryOptions,
this.previousData.observableQueryOptions
)
) {
// When comparing new options against previously stored options,
// we'll ignore callback functions since their identities are not
// stable, meaning they'll always show as being different. Ignoring
// them when determining if options have changed is okay however, as
// callback functions are not normally changed between renders.
if (!equal(
stripFns(newObservableQueryOptions),
stripFns(this.previousData.observableQueryOptions),
)) {
this.previousData.observableQueryOptions = newObservableQueryOptions;
this.currentObservable
.setOptions(newObservableQueryOptions)
Expand Down
37 changes: 37 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1819,6 +1819,43 @@ describe('useQuery Hook', () => {

return wait().then(resolve, reject);
});

itAsync(
'should not make extra network requests when `onCompleted` is ' +
'defined with a `network-only` fetch policy',
(resolve, reject) => {
let renderCount = 0;
function Component() {
const { loading, data } = useQuery(CAR_QUERY, {
fetchPolicy: 'network-only',
onCompleted: () => undefined
});
switch (++renderCount) {
case 1:
expect(loading).toBeTruthy();
break;
case 2:
expect(loading).toBeFalsy();
expect(data).toEqual(CAR_RESULT_DATA);
break;
case 3:
fail('Too many renders');
default:
}
return null;
}

render(
<MockedProvider mocks={CAR_MOCKS}>
<Component />
</MockedProvider>
);

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

describe('Optimistic data', () => {
Expand Down

0 comments on commit d7dedc9

Please sign in to comment.