Skip to content

Commit

Permalink
Refetch should not return partial data with errorPolicy none (#10321)
Browse files Browse the repository at this point in the history
* chore: adds failing test for issue 10317

* chore: reset data to undefined on refetch with errorPolicy: none

* chore: use watchQuery.errorPolicy for default value

* chore: clean up test

* chore: adds changeset

* fix: shouldNotify should be false if errorPolicy: none and missing errors

* fix: do not return cache data if errorPolicy none on refetch and missing errors

* chore: undo whitespace removal

* chore: add comment and test
  • Loading branch information
alessbell authored Jan 20, 2023
1 parent 0b07aa9 commit bbaa3ef
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-pears-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

Refetch should not return partial data with `errorPolicy: none` and `notifyOnNetworkStatusChange: true`.
12 changes: 12 additions & 0 deletions src/core/QueryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,18 @@ export class QueryManager<TStore> {
}).then(resolved => fromData(resolved.data || void 0));
}

// Resolves https://github.com/apollographql/apollo-client/issues/10317.
// If errorPolicy is 'none' and notifyOnNetworkStatusChange is true,
// data was incorrectly returned from the cache on refetch:
// if diff.missing exists, we should not return cache data.
if (
errorPolicy === 'none' &&
networkStatus === NetworkStatus.refetch &&
Array.isArray(diff.missing)
) {
return fromData(void 0);
}

return fromData(data);
};

Expand Down
224 changes: 224 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Fragment, useEffect, useState } from 'react';
import { DocumentNode, GraphQLError } from 'graphql';
import gql from 'graphql-tag';
import { act } from 'react-dom/test-utils';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor, renderHook } from '@testing-library/react';
import {
ApolloClient,
Expand Down Expand Up @@ -2399,6 +2400,229 @@ describe('useQuery Hook', () => {
}, { interval: 1, timeout: 20 })).rejects.toThrow()
});

it('should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true', async () => {
const query = gql`
{
dogs {
id
breed
}
}
`;

const GET_DOG_DETAILS = gql`
query dog($breed: String!) {
dog(breed: $breed) {
id
unexisting
}
dogs {
id
breed
}
}
`;

const dogData = [
{
"id": "Z1fdFgU",
"breed": "affenpinscher",
"__typename": "Dog"
},
{
"id": "ZNDtCU",
"breed": "airedale",
"__typename": "Dog"
},
];

const detailsMock = (breed: string) => ({
request: { query: GET_DOG_DETAILS, variables: { breed } },
result: {
errors: [new GraphQLError(`Cannot query field "unexisting" on type "Dog".`)],
},
});

const mocks = [
{
request: { query },
result: { data: { dogs: dogData } },
},
// use the same mock for the initial query on select change
// and subsequent refetch() call
detailsMock('airedale'),
detailsMock('airedale'),
];
const Dogs: React.FC<{
onDogSelected: (event: React.ChangeEvent<HTMLSelectElement>) => void;
}> = ({ onDogSelected }) => {
const { loading, error, data } = useQuery<
{ dogs: { id: string; breed: string; }[] }
>(query);

if (loading) return <>Loading...</>;
if (error) return <>{`Error! ${error.message}`}</>;

return (
<select name="dog" onChange={onDogSelected}>
{data?.dogs.map((dog) => (
<option key={dog.id} value={dog.breed}>
{dog.breed}
</option>
))}
</select>
);
};

const DogDetails: React.FC<{
breed: string;
}> = ({ breed }) => {
const { loading, error, data, refetch, networkStatus } = useQuery(
GET_DOG_DETAILS,
{
variables: { breed },
notifyOnNetworkStatusChange: true
}
);
if (networkStatus === 4) return <p>Refetching!</p>;
if (loading) return <p>Loading!</p>;
return (
<div>
<div>
{data ? 'Partial data rendered' : null}
</div>

<div>
{error ? (
`Error!: ${error}`
) : (
'Rendering!'
)}
</div>
<button onClick={() => refetch()}>Refetch!</button>
</div>
);
};

const ParentComponent: React.FC = () => {
const [selectedDog, setSelectedDog] = useState<null | string>(null);
function onDogSelected(event: React.ChangeEvent<HTMLSelectElement>) {
setSelectedDog(event.target.value);
}
return (
<MockedProvider mocks={mocks}>
<div>
{selectedDog && <DogDetails breed={selectedDog} />}
<Dogs onDogSelected={onDogSelected} />
</div>
</MockedProvider>
);
};

render(<ParentComponent />);

// on initial load, the list of dogs populates the dropdown
await screen.findByText('affenpinscher');

// the user selects a different dog from the dropdown which
// fires the GET_DOG_DETAILS query, retuning an error
const user = userEvent.setup();
await user.selectOptions(
screen.getByRole('combobox'),
screen.getByRole('option', { name: 'airedale' })
);

// With the default errorPolicy of 'none', the error is rendered
// and partial data is not
await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
expect(screen.queryByText(/partial data rendered/i)).toBeNull();

// When we call refetch...
await user.click(screen.getByRole('button', { name: /Refetch!/i }))

// The error is still present, and partial data still not rendered
await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
expect(screen.queryByText(/partial data rendered/i)).toBeNull();
});

it('should return partial data from cache on refetch', async () => {
const GET_DOG_DETAILS = gql`
query dog($breed: String!) {
dog(breed: $breed) {
id
}
}
`;
const detailsMock = (breed: string) => ({
request: { query: GET_DOG_DETAILS, variables: { breed } },
result: {
data: {
dog: {
"id": "ZNDtCU",
"__typename": "Dog"
}
}
},
});

const mocks = [
// use the same mock for the initial query on select change
// and subsequent refetch() call
detailsMock('airedale'),
detailsMock('airedale'),
];

const DogDetails: React.FC<{
breed?: string;
}> = ({ breed = "airedale" }) => {
const { data, refetch, networkStatus } = useQuery(
GET_DOG_DETAILS,
{
variables: { breed },
notifyOnNetworkStatusChange: true
}
);
if (networkStatus === 1) return <p>Loading!</p>;
return (
// Render existing results, but dim the UI until the results
// have finished loading...
<div style={{ opacity: networkStatus === 4 ? 0.5 : 1 }}>
<div>
{data ? 'Data rendered' : null}
</div>
<button onClick={() => refetch()}>Refetch!</button>
</div>
);
};

const ParentComponent: React.FC = () => {
return (
<MockedProvider mocks={mocks}>
<DogDetails />
</MockedProvider>
);
};

render(<ParentComponent />);

const user = userEvent.setup();

await waitFor(() => {
expect(screen.getByText('Loading!')).toBeTruthy();
}, { interval: 1 });

await waitFor(() => {
expect(screen.getByText('Data rendered')).toBeTruthy();
}, { interval: 1 });

// When we call refetch...
await user.click(screen.getByRole('button', { name: /Refetch!/i }))

// Data from the cache remains onscreen while network request
// is made
expect(screen.getByText('Data rendered')).toBeTruthy();
});

it('should persist errors on re-render with inline onError/onCompleted callbacks', async () => {
const query = gql`{ hello }`;
const mocks = [
Expand Down

0 comments on commit bbaa3ef

Please sign in to comment.