Skip to content

Commit bbaa3ef

Browse files
authored
Refetch should not return partial data with errorPolicy none (apollographql#10321)
* 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
1 parent 0b07aa9 commit bbaa3ef

3 files changed

Lines changed: 241 additions & 0 deletions

File tree

.changeset/rotten-pears-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Refetch should not return partial data with `errorPolicy: none` and `notifyOnNetworkStatusChange: true`.

src/core/QueryManager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,6 +1453,18 @@ export class QueryManager<TStore> {
14531453
}).then(resolved => fromData(resolved.data || void 0));
14541454
}
14551455

1456+
// Resolves https://github.com/apollographql/apollo-client/issues/10317.
1457+
// If errorPolicy is 'none' and notifyOnNetworkStatusChange is true,
1458+
// data was incorrectly returned from the cache on refetch:
1459+
// if diff.missing exists, we should not return cache data.
1460+
if (
1461+
errorPolicy === 'none' &&
1462+
networkStatus === NetworkStatus.refetch &&
1463+
Array.isArray(diff.missing)
1464+
) {
1465+
return fromData(void 0);
1466+
}
1467+
14561468
return fromData(data);
14571469
};
14581470

src/react/hooks/__tests__/useQuery.test.tsx

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Fragment, useEffect, useState } from 'react';
22
import { DocumentNode, GraphQLError } from 'graphql';
33
import gql from 'graphql-tag';
44
import { act } from 'react-dom/test-utils';
5+
import userEvent from '@testing-library/user-event';
56
import { render, screen, waitFor, renderHook } from '@testing-library/react';
67
import {
78
ApolloClient,
@@ -2399,6 +2400,229 @@ describe('useQuery Hook', () => {
23992400
}, { interval: 1, timeout: 20 })).rejects.toThrow()
24002401
});
24012402

2403+
it('should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true', async () => {
2404+
const query = gql`
2405+
{
2406+
dogs {
2407+
id
2408+
breed
2409+
}
2410+
}
2411+
`;
2412+
2413+
const GET_DOG_DETAILS = gql`
2414+
query dog($breed: String!) {
2415+
dog(breed: $breed) {
2416+
id
2417+
unexisting
2418+
}
2419+
dogs {
2420+
id
2421+
breed
2422+
}
2423+
}
2424+
`;
2425+
2426+
const dogData = [
2427+
{
2428+
"id": "Z1fdFgU",
2429+
"breed": "affenpinscher",
2430+
"__typename": "Dog"
2431+
},
2432+
{
2433+
"id": "ZNDtCU",
2434+
"breed": "airedale",
2435+
"__typename": "Dog"
2436+
},
2437+
];
2438+
2439+
const detailsMock = (breed: string) => ({
2440+
request: { query: GET_DOG_DETAILS, variables: { breed } },
2441+
result: {
2442+
errors: [new GraphQLError(`Cannot query field "unexisting" on type "Dog".`)],
2443+
},
2444+
});
2445+
2446+
const mocks = [
2447+
{
2448+
request: { query },
2449+
result: { data: { dogs: dogData } },
2450+
},
2451+
// use the same mock for the initial query on select change
2452+
// and subsequent refetch() call
2453+
detailsMock('airedale'),
2454+
detailsMock('airedale'),
2455+
];
2456+
const Dogs: React.FC<{
2457+
onDogSelected: (event: React.ChangeEvent<HTMLSelectElement>) => void;
2458+
}> = ({ onDogSelected }) => {
2459+
const { loading, error, data } = useQuery<
2460+
{ dogs: { id: string; breed: string; }[] }
2461+
>(query);
2462+
2463+
if (loading) return <>Loading...</>;
2464+
if (error) return <>{`Error! ${error.message}`}</>;
2465+
2466+
return (
2467+
<select name="dog" onChange={onDogSelected}>
2468+
{data?.dogs.map((dog) => (
2469+
<option key={dog.id} value={dog.breed}>
2470+
{dog.breed}
2471+
</option>
2472+
))}
2473+
</select>
2474+
);
2475+
};
2476+
2477+
const DogDetails: React.FC<{
2478+
breed: string;
2479+
}> = ({ breed }) => {
2480+
const { loading, error, data, refetch, networkStatus } = useQuery(
2481+
GET_DOG_DETAILS,
2482+
{
2483+
variables: { breed },
2484+
notifyOnNetworkStatusChange: true
2485+
}
2486+
);
2487+
if (networkStatus === 4) return <p>Refetching!</p>;
2488+
if (loading) return <p>Loading!</p>;
2489+
return (
2490+
<div>
2491+
<div>
2492+
{data ? 'Partial data rendered' : null}
2493+
</div>
2494+
2495+
<div>
2496+
{error ? (
2497+
`Error!: ${error}`
2498+
) : (
2499+
'Rendering!'
2500+
)}
2501+
</div>
2502+
<button onClick={() => refetch()}>Refetch!</button>
2503+
</div>
2504+
);
2505+
};
2506+
2507+
const ParentComponent: React.FC = () => {
2508+
const [selectedDog, setSelectedDog] = useState<null | string>(null);
2509+
function onDogSelected(event: React.ChangeEvent<HTMLSelectElement>) {
2510+
setSelectedDog(event.target.value);
2511+
}
2512+
return (
2513+
<MockedProvider mocks={mocks}>
2514+
<div>
2515+
{selectedDog && <DogDetails breed={selectedDog} />}
2516+
<Dogs onDogSelected={onDogSelected} />
2517+
</div>
2518+
</MockedProvider>
2519+
);
2520+
};
2521+
2522+
render(<ParentComponent />);
2523+
2524+
// on initial load, the list of dogs populates the dropdown
2525+
await screen.findByText('affenpinscher');
2526+
2527+
// the user selects a different dog from the dropdown which
2528+
// fires the GET_DOG_DETAILS query, retuning an error
2529+
const user = userEvent.setup();
2530+
await user.selectOptions(
2531+
screen.getByRole('combobox'),
2532+
screen.getByRole('option', { name: 'airedale' })
2533+
);
2534+
2535+
// With the default errorPolicy of 'none', the error is rendered
2536+
// and partial data is not
2537+
await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
2538+
expect(screen.queryByText(/partial data rendered/i)).toBeNull();
2539+
2540+
// When we call refetch...
2541+
await user.click(screen.getByRole('button', { name: /Refetch!/i }))
2542+
2543+
// The error is still present, and partial data still not rendered
2544+
await screen.findByText('Error!: ApolloError: Cannot query field "unexisting" on type "Dog".')
2545+
expect(screen.queryByText(/partial data rendered/i)).toBeNull();
2546+
});
2547+
2548+
it('should return partial data from cache on refetch', async () => {
2549+
const GET_DOG_DETAILS = gql`
2550+
query dog($breed: String!) {
2551+
dog(breed: $breed) {
2552+
id
2553+
}
2554+
}
2555+
`;
2556+
const detailsMock = (breed: string) => ({
2557+
request: { query: GET_DOG_DETAILS, variables: { breed } },
2558+
result: {
2559+
data: {
2560+
dog: {
2561+
"id": "ZNDtCU",
2562+
"__typename": "Dog"
2563+
}
2564+
}
2565+
},
2566+
});
2567+
2568+
const mocks = [
2569+
// use the same mock for the initial query on select change
2570+
// and subsequent refetch() call
2571+
detailsMock('airedale'),
2572+
detailsMock('airedale'),
2573+
];
2574+
2575+
const DogDetails: React.FC<{
2576+
breed?: string;
2577+
}> = ({ breed = "airedale" }) => {
2578+
const { data, refetch, networkStatus } = useQuery(
2579+
GET_DOG_DETAILS,
2580+
{
2581+
variables: { breed },
2582+
notifyOnNetworkStatusChange: true
2583+
}
2584+
);
2585+
if (networkStatus === 1) return <p>Loading!</p>;
2586+
return (
2587+
// Render existing results, but dim the UI until the results
2588+
// have finished loading...
2589+
<div style={{ opacity: networkStatus === 4 ? 0.5 : 1 }}>
2590+
<div>
2591+
{data ? 'Data rendered' : null}
2592+
</div>
2593+
<button onClick={() => refetch()}>Refetch!</button>
2594+
</div>
2595+
);
2596+
};
2597+
2598+
const ParentComponent: React.FC = () => {
2599+
return (
2600+
<MockedProvider mocks={mocks}>
2601+
<DogDetails />
2602+
</MockedProvider>
2603+
);
2604+
};
2605+
2606+
render(<ParentComponent />);
2607+
2608+
const user = userEvent.setup();
2609+
2610+
await waitFor(() => {
2611+
expect(screen.getByText('Loading!')).toBeTruthy();
2612+
}, { interval: 1 });
2613+
2614+
await waitFor(() => {
2615+
expect(screen.getByText('Data rendered')).toBeTruthy();
2616+
}, { interval: 1 });
2617+
2618+
// When we call refetch...
2619+
await user.click(screen.getByRole('button', { name: /Refetch!/i }))
2620+
2621+
// Data from the cache remains onscreen while network request
2622+
// is made
2623+
expect(screen.getByText('Data rendered')).toBeTruthy();
2624+
});
2625+
24022626
it('should persist errors on re-render with inline onError/onCompleted callbacks', async () => {
24032627
const query = gql`{ hello }`;
24042628
const mocks = [

0 commit comments

Comments
 (0)