Skip to content

Commit 45c47be

Browse files
authored
useLazyQuery: fix rules of React violations (#11851)
* useLazyQuery: fix rules of React violations * size-limits * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas <phryneas@users.noreply.github.com>
1 parent 8740f19 commit 45c47be

File tree

5 files changed

+48
-27
lines changed

5 files changed

+48
-27
lines changed

.changeset/shaggy-mirrors-judge.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Avoid usage of useRef in useInternalState to prevent ref access in render.

.changeset/stupid-planes-nail.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
Fix a bug where `useLazyQuery` would not pick up a client change.

.size-limits.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"dist/apollo-client.min.cjs": 39574,
2+
"dist/apollo-client.min.cjs": 39607,
33
"import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32821
44
}

src/react/hooks/useLazyQuery.ts

+15-11
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
LazyQueryHookOptions,
1010
LazyQueryResultTuple,
1111
NoInfer,
12-
QueryResult,
1312
} from "../types/types.js";
1413
import { useInternalState } from "./useQuery.js";
1514
import { useApolloClient } from "./useApolloClient.js";
@@ -95,30 +94,35 @@ export function useLazyQuery<
9594
useQueryResult.observable.options.initialFetchPolicy ||
9695
internalState.getDefaultFetchPolicy();
9796

98-
const result: QueryResult<TData, TVariables> = Object.assign(useQueryResult, {
99-
called: !!execOptionsRef.current,
100-
});
101-
97+
const { forceUpdateState, obsQueryFields } = internalState;
10298
// We use useMemo here to make sure the eager methods have a stable identity.
10399
const eagerMethods = React.useMemo(() => {
104100
const eagerMethods: Record<string, any> = {};
105101
for (const key of EAGER_METHODS) {
106-
const method = result[key];
102+
const method = obsQueryFields[key];
107103
eagerMethods[key] = function () {
108104
if (!execOptionsRef.current) {
109105
execOptionsRef.current = Object.create(null);
110106
// Only the first time populating execOptionsRef.current matters here.
111-
internalState.forceUpdateState();
107+
forceUpdateState();
112108
}
113109
// @ts-expect-error this is just too generic to type
114110
return method.apply(this, arguments);
115111
};
116112
}
117113

118114
return eagerMethods;
119-
}, []);
120-
121-
Object.assign(result, eagerMethods);
115+
}, [forceUpdateState, obsQueryFields]);
116+
117+
const called = !!execOptionsRef.current;
118+
const result = React.useMemo(
119+
() => ({
120+
...useQueryResult,
121+
...eagerMethods,
122+
called,
123+
}),
124+
[useQueryResult, eagerMethods, called]
125+
);
122126

123127
const execute = React.useCallback<LazyQueryResultTuple<TData, TVariables>[0]>(
124128
(executeOptions) => {
@@ -147,7 +151,7 @@ export function useLazyQuery<
147151

148152
return promise;
149153
},
150-
[]
154+
[eagerMethods, initialFetchPolicy, internalState]
151155
);
152156

153157
return [execute, result];

src/react/hooks/useQuery.ts

+22-15
Original file line numberDiff line numberDiff line change
@@ -109,23 +109,30 @@ export function useInternalState<TData, TVariables extends OperationVariables>(
109109
client: ApolloClient<any>,
110110
query: DocumentNode | TypedDocumentNode<TData, TVariables>
111111
): InternalState<TData, TVariables> {
112-
const stateRef = React.useRef<InternalState<TData, TVariables>>();
113-
if (
114-
!stateRef.current ||
115-
client !== stateRef.current.client ||
116-
query !== stateRef.current.query
117-
) {
118-
stateRef.current = new InternalState(client, query, stateRef.current);
119-
}
120-
const state = stateRef.current;
121-
122112
// By default, InternalState.prototype.forceUpdate is an empty function, but
123113
// we replace it here (before anyone has had a chance to see this state yet)
124114
// with a function that unconditionally forces an update, using the latest
125-
// setTick function. Updating this state by calling state.forceUpdate is the
126-
// only way we trigger React component updates (no other useState calls within
127-
// the InternalState class).
128-
state.forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1];
115+
// setTick function. Updating this state by calling state.forceUpdate or the
116+
// uSES notification callback are the only way we trigger React component updates.
117+
const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1];
118+
119+
function createInternalState(previous?: InternalState<TData, TVariables>) {
120+
return Object.assign(new InternalState(client, query, previous), {
121+
forceUpdateState,
122+
});
123+
}
124+
125+
let [state, updateState] = React.useState(createInternalState);
126+
127+
if (client !== state.client || query !== state.query) {
128+
// If the client or query have changed, we need to create a new InternalState.
129+
// This will trigger a re-render with the new state, but it will also continue
130+
// to run the current render function to completion.
131+
// Since we sometimes trigger some side-effects in the render function, we
132+
// re-assign `state` to the new state to ensure that those side-effects are
133+
// triggered with the new state.
134+
updateState((state = createInternalState(state)));
135+
}
129136

130137
return state;
131138
}
@@ -511,7 +518,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
511518
private onError(error: ApolloError) {}
512519

513520
private observable!: ObservableQuery<TData, TVariables>;
514-
private obsQueryFields!: Omit<
521+
public obsQueryFields!: Omit<
515522
ObservableQueryFields<TData, TVariables>,
516523
"variables"
517524
>;

0 commit comments

Comments
 (0)