Skip to content

Commit d225218

Browse files
committed
Fix a potential crash when calling clearStore while a query was running.
1 parent 3e6990d commit d225218

File tree

5 files changed

+131
-2
lines changed

5 files changed

+131
-2
lines changed

.changeset/pink-guests-vanish.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Fix a potential crash when calling `clearStore` while a query was running.
6+
7+
Previously, calling `client.clearStore()` while a query was running had one of these results:
8+
* `useQuery` would stay in a `loading: true` state.
9+
* `useLazyQuery` would stay in a `loading: true` state, but also crash with a `"Cannot read property 'data' of undefined"` error.
10+
11+
Now, in both cases, the hook will enter an error state with a `networkError`, and the promise returned by the `useLazyQuery` `execute` function will return a result in an error state.

src/core/ObservableQuery.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
fixObservableSubclass,
1818
getQueryDefinition,
1919
} from "../utilities/index.js";
20-
import type { ApolloError } from "../errors/index.js";
20+
import { ApolloError, isApolloError } from "../errors/index.js";
2121
import type { QueryManager } from "./QueryManager.js";
2222
import type {
2323
ApolloQueryResult,
@@ -940,6 +940,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`,
940940
},
941941
error: (error) => {
942942
if (equal(this.variables, variables)) {
943+
// Coming from `getResultsFromLink`, `error` here should always be an `ApolloError`.
944+
// However, calling `concast.cancel` can inject another type of error, so we have to
945+
// wrap it again here.
946+
if (!isApolloError(error)) {
947+
error = new ApolloError({ networkError: error });
948+
}
943949
finishWaitingForOwnResult();
944950
this.reportError(error, variables);
945951
}

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { useLazyQuery } from "../useLazyQuery";
2626
import { QueryResult } from "../../types/types";
2727
import { profileHook } from "../../../testing/internal";
28+
import { InvariantError } from "../../../utilities/globals";
2829

2930
describe("useLazyQuery Hook", () => {
3031
const helloQuery: TypedDocumentNode<{
@@ -1922,6 +1923,68 @@ describe("useLazyQuery Hook", () => {
19221923
expect(options.fetchPolicy).toBe(defaultFetchPolicy);
19231924
});
19241925
});
1926+
1927+
test("regression for #11988: calling `clearStore` while a lazy query is running puts the hook into an error state and resolves the promise with an error result", async () => {
1928+
const link = new MockSubscriptionLink();
1929+
let requests = 0;
1930+
link.onSetup(() => requests++);
1931+
const client = new ApolloClient({
1932+
link,
1933+
cache: new InMemoryCache(),
1934+
});
1935+
const ProfiledHook = profileHook(() => useLazyQuery(helloQuery));
1936+
render(<ProfiledHook />, {
1937+
wrapper: ({ children }) => (
1938+
<ApolloProvider client={client}>{children}</ApolloProvider>
1939+
),
1940+
});
1941+
1942+
{
1943+
const [, result] = await ProfiledHook.takeSnapshot();
1944+
expect(result.loading).toBe(false);
1945+
expect(result.data).toBeUndefined();
1946+
}
1947+
const execute = ProfiledHook.getCurrentSnapshot()[0];
1948+
1949+
const promise = execute();
1950+
expect(requests).toBe(1);
1951+
1952+
{
1953+
const [, result] = await ProfiledHook.takeSnapshot();
1954+
expect(result.loading).toBe(true);
1955+
expect(result.data).toBeUndefined();
1956+
}
1957+
1958+
client.clearStore();
1959+
1960+
const executionResult = await promise;
1961+
expect(executionResult.data).toBeUndefined();
1962+
expect(executionResult.loading).toBe(true);
1963+
expect(executionResult.error).toEqual(
1964+
new ApolloError({
1965+
networkError: new InvariantError(
1966+
"Store reset while query was in flight (not completed in link chain)"
1967+
),
1968+
})
1969+
);
1970+
1971+
{
1972+
const [, result] = await ProfiledHook.takeSnapshot();
1973+
expect(result.loading).toBe(false);
1974+
expect(result.data).toBeUndefined();
1975+
expect(result.error).toEqual(
1976+
new ApolloError({
1977+
networkError: new InvariantError(
1978+
"Store reset while query was in flight (not completed in link chain)"
1979+
),
1980+
})
1981+
);
1982+
}
1983+
1984+
link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
1985+
await expect(ProfiledHook).not.toRerender({ timeout: 50 });
1986+
expect(requests).toBe(1);
1987+
});
19251988
});
19261989

19271990
describe.skip("Type Tests", () => {

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
import { useApolloClient } from "../useApolloClient";
3939
import { useLazyQuery } from "../useLazyQuery";
4040
import { mockFetchQuery } from "../../../core/__tests__/ObservableQuery";
41+
import { newInvariantError } from "../../../utilities/globals";
4142

4243
const IS_REACT_17 = React.version.startsWith("17");
4344

@@ -9786,6 +9787,54 @@ describe("useQuery Hook", () => {
97869787
}
97879788
);
97889789
});
9790+
9791+
test("calling `clearStore` while a query is running puts the hook into an error state", async () => {
9792+
const query = gql`
9793+
query {
9794+
hello
9795+
}
9796+
`;
9797+
9798+
const link = new MockSubscriptionLink();
9799+
let requests = 0;
9800+
link.onSetup(() => requests++);
9801+
const client = new ApolloClient({
9802+
link,
9803+
cache: new InMemoryCache(),
9804+
});
9805+
const ProfiledHook = profileHook(() => useQuery(query));
9806+
render(<ProfiledHook />, {
9807+
wrapper: ({ children }) => (
9808+
<ApolloProvider client={client}>{children}</ApolloProvider>
9809+
),
9810+
});
9811+
9812+
expect(requests).toBe(1);
9813+
{
9814+
const result = await ProfiledHook.takeSnapshot();
9815+
expect(result.loading).toBe(true);
9816+
expect(result.data).toBeUndefined();
9817+
}
9818+
9819+
client.clearStore();
9820+
9821+
{
9822+
const result = await ProfiledHook.takeSnapshot();
9823+
expect(result.loading).toBe(false);
9824+
expect(result.data).toBeUndefined();
9825+
expect(result.error).toEqual(
9826+
new ApolloError({
9827+
networkError: new newInvariantError(
9828+
"Store reset while query was in flight (not completed in link chain)"
9829+
),
9830+
})
9831+
);
9832+
}
9833+
9834+
link.simulateResult({ result: { data: { hello: "Greetings" } } }, true);
9835+
await expect(ProfiledHook).not.toRerender({ timeout: 50 });
9836+
expect(requests).toBe(1);
9837+
});
97899838
});
97909839

97919840
describe.skip("Type Tests", () => {

src/utilities/observables/Concast.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export class Concast<T> extends Observable<T> {
256256
public cancel = (reason: any) => {
257257
this.reject(reason);
258258
this.sources = [];
259-
this.handlers.complete();
259+
this.handlers.error(reason);
260260
};
261261
}
262262

0 commit comments

Comments
 (0)