Skip to content

Commit

Permalink
Better handle cached data with deferred queries
Browse files Browse the repository at this point in the history
When using `useQuery` with deferred queries that already have cache data
written, the initial chunk of data would overwrite anything in the
cache, which meant cached data for deferred chunks would disappear. This
is now better handled by merging existing cache data with the initial
deferred chunk to ensure a complete result set is still returned.
  • Loading branch information
jerelmiller committed Dec 6, 2022
1 parent 39d83c9 commit b1e558c
Show file tree
Hide file tree
Showing 2 changed files with 208 additions and 1 deletion.
13 changes: 12 additions & 1 deletion src/core/QueryInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ export class QueryInfo {
| "errorPolicy">,
cacheWriteBehavior: CacheWriteBehavior,
) {
const merger = new DeepMerger();
const graphQLErrors = isNonEmptyArray(result.errors)
? result.errors.slice(0)
: [];
Expand All @@ -371,9 +372,19 @@ export class QueryInfo {
// requests. To allow future notify timeouts, diff and dirty are reset as well.
this.reset();

// Detect the first chunk of a deferred query and merge it with existing
// cache data. This ensures a `cache-first` fetch policy that returns
// partial cache data or a `cache-and-network` fetch policy that already
// has full data in the cache does not complain when trying to merge the
// initial deferred server data with existing cache data.
if ('hasNext' in result && result.hasNext && !('incremental' in result)) {
const diff = this.getDiff();
result.data = merger.merge(diff.result, result.data)
}

if ('incremental' in result && isNonEmptyArray(result.incremental)) {
let mergedData = this.getDiff().result;
const merger = new DeepMerger();

result.incremental.forEach(({ data, path, errors }) => {
for (let i = path.length - 1; i >= 0; --i) {
const key = path[i];
Expand Down
196 changes: 196 additions & 0 deletions src/react/hooks/__tests__/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5647,5 +5647,201 @@ describe('useQuery Hook', () => {
}
});
});

it('returns eventually consistent data from deferred queries with data in the cache while using a "cache-and-network" fetch policy', async () => {
const query = gql`
query {
greeting {
message
... on Greeting @defer {
recipient {
name
}
}
}
}
`;

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

cache.writeQuery({
query,
data: {
greeting: {
__typename: 'Greeting',
message: 'Hello cached',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
},
});

const { result, waitForNextUpdate } = renderHook(
() => useQuery(query, { fetchPolicy: 'cache-and-network' }),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
),
}
);

expect(result.current.loading).toBe(true);
expect(result.current.networkStatus).toBe(NetworkStatus.loading);
expect(result.current.data).toEqual({
greeting: {
message: 'Hello cached',
__typename: 'Greeting',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
});

link.simulateResult({
result: {
data: { greeting: { __typename: 'Greeting', message: 'Hello world' } },
hasNext: true,
},
});

await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.networkStatus).toBe(NetworkStatus.ready);
expect(result.current.data).toEqual({
greeting: {
__typename: 'Greeting',
message: 'Hello world',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
});

link.simulateResult({
result: {
incremental: [
{
data: {
recipient: { name: 'Alice', __typename: 'Person' },
__typename: 'Greeting',
},
path: ['greeting'],
},
],
hasNext: false,
},
});

await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.networkStatus).toBe(NetworkStatus.ready);
expect(result.current.data).toEqual({
greeting: {
__typename: 'Greeting',
message: 'Hello world',
recipient: { __typename: 'Person', name: 'Alice' },
},
});
});

it('returns eventually consistent data from deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => {
const query = gql`
query {
greeting {
message
... on Greeting @defer {
recipient {
name
}
}
}
}
`;

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

// We know we are writing partial data to the cache so suppress the console
// warning.
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
cache.writeQuery({
query,
data: {
greeting: {
__typename: 'Greeting',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
},
});
consoleSpy.mockRestore();

const { result, waitForNextUpdate } = renderHook(
() =>
useQuery(query, {
fetchPolicy: 'cache-first',
returnPartialData: true
}),
{
wrapper: ({ children }) => (
<ApolloProvider client={client}>
{children}
</ApolloProvider>
),
}
);

expect(result.current.loading).toBe(true);
expect(result.current.networkStatus).toBe(NetworkStatus.loading);
expect(result.current.data).toEqual({
greeting: {
__typename: 'Greeting',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
});

link.simulateResult({
result: {
data: { greeting: { message: 'Hello world', __typename: 'Greeting' } },
hasNext: true,
},
});

await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.networkStatus).toBe(NetworkStatus.ready);
expect(result.current.data).toEqual({
greeting: {
__typename: 'Greeting',
message: 'Hello world',
recipient: { __typename: 'Person', name: 'Cached Alice' },
},
});

link.simulateResult({
result: {
incremental: [
{
data: {
__typename: 'Greeting',
recipient: { name: 'Alice', __typename: 'Person' },
},
path: ['greeting'],
},
],
hasNext: false,
},
});

await waitForNextUpdate();
expect(result.current.loading).toBe(false);
expect(result.current.networkStatus).toBe(NetworkStatus.ready);
expect(result.current.data).toEqual({
greeting: {
__typename: 'Greeting',
message: 'Hello world',
recipient: { __typename: 'Person', name: 'Alice' },
},
});
});
});
});

0 comments on commit b1e558c

Please sign in to comment.