Skip to content

Commit 6536369

Browse files
authored
enable react-hooks lint rules (#11511)
* enable `react-hooks` lint rules * Clean up Prettier, Size-limit, and Api-Extractor * update eslint rule * different type casting to not break lint rule parsing * fix up useMutation + lifecycle test * Clean up Prettier, Size-limit, and Api-Extractor * avoid reading/writing ref in render * fix up useLazyQuery * Clean up Prettier, Size-limit, and Api-Extractor * fix up another test doing a mutation in render * more useQuery cleanup * Clean up Prettier, Size-limit, and Api-Extractor * ignore rule of hook for context access * undo changes that were moved into separate PRs * Clean up Prettier, Size-limit, and Api-Extractor * almost full rewrite of the `useSubscription` hook * Clean up Prettier, Size-limit, and Api-Extractor * ignore any rule violation lint warnings in useSubscription and useQuery * changeset * rename patch * also add "eslint-plugin-react-compiler" rules --------- Co-authored-by: phryneas <phryneas@users.noreply.github.com>
1 parent 08dbc02 commit 6536369

13 files changed

+738
-225
lines changed

.changeset/famous-camels-rescue.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": patch
3+
---
4+
5+
`useLoadableQuery`: ensure that `loadQuery` is updated if the ApolloClient instance changes

.eslintrc

+3
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
{
2424
"files": ["**/*.ts", "**/*.tsx"],
2525
"excludedFiles": ["**/__tests__/**/*.*", "*.d.ts"],
26+
"extends": ["plugin:react-hooks/recommended"],
2627
"parserOptions": {
2728
"project": "./tsconfig.json"
2829
},
30+
"plugins": ["eslint-plugin-react-compiler"],
2931
"rules": {
32+
"react-compiler/react-compiler": "error",
3033
"@typescript-eslint/consistent-type-imports": [
3134
"error",
3235
{

package-lock.json

+660-215
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@
148148
"eslint-import-resolver-typescript": "3.6.1",
149149
"eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c",
150150
"eslint-plugin-local-rules": "2.0.1",
151+
"eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
152+
"eslint-plugin-react-hooks": "4.6.2",
151153
"eslint-plugin-testing-library": "6.2.2",
152154
"expect-type": "0.19.0",
153155
"fetch-mock": "9.11.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
diff --git a/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js b/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js
2+
index 441442f..d1ec5dc 100644
3+
--- a/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js
4+
+++ b/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js
5+
@@ -905,7 +905,7 @@ var ExhaustiveDeps = {
6+
var _callee = callee,
7+
name = _callee.name;
8+
9+
- if (name === 'useRef' && id.type === 'Identifier') {
10+
+ if ((name === 'useRef' || name === "useLazyRef") && id.type === 'Identifier') {
11+
// useRef() return value is stable.
12+
return true;
13+
} else if (name === 'useState' || name === 'useReducer') {
14+
diff --git a/node_modules/eslint-plugin-react-hooks/index.js b/node_modules/eslint-plugin-react-hooks/index.js
15+
index 0e91baf..7e86d46 100644
16+
--- a/node_modules/eslint-plugin-react-hooks/index.js
17+
+++ b/node_modules/eslint-plugin-react-hooks/index.js
18+
@@ -1,9 +1,3 @@
19+
'use strict';
20+
21+
-// TODO: this doesn't make sense for an ESLint rule.
22+
-// We need to fix our build process to not create bundles for "raw" packages like this.
23+
-if (process.env.NODE_ENV === 'production') {
24+
- module.exports = require('./cjs/eslint-plugin-react-hooks.production.min.js');
25+
-} else {
26+
- module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
27+
-}
28+
+module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');

src/react/hooks/internal/useRenderGuard.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Relay does this too, so we hope this is safe.
1212
https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98
1313
*/
1414
export function useRenderGuard() {
15+
// eslint-disable-next-line react-compiler/react-compiler
1516
RenderDispatcher = getRenderDispatcher();
1617

1718
return React.useCallback(() => {

src/react/hooks/useLoadableQuery.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -245,12 +245,19 @@ export function useLoadableQuery<
245245

246246
setQueryRef(wrapQueryRef(queryRef));
247247
},
248-
[query, queryKey, suspenseCache, watchQueryOptions, calledDuringRender]
248+
[
249+
query,
250+
queryKey,
251+
suspenseCache,
252+
watchQueryOptions,
253+
calledDuringRender,
254+
client,
255+
]
249256
);
250257

251258
const reset: ResetFunction = React.useCallback(() => {
252259
setQueryRef(null);
253-
}, [queryRef]);
260+
}, []);
254261

255262
return [loadQuery, queryRef, { fetchMore, refetch, reset }];
256263
}

src/react/hooks/useQuery.ts

+6
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,16 @@ class InternalState<TData, TVariables extends OperationVariables> {
230230
// initialization, this.renderPromises is usually undefined (unless SSR is
231231
// happening), but that's fine as long as it has been initialized that way,
232232
// rather than left uninitialized.
233+
// eslint-disable-next-line react-hooks/rules-of-hooks
233234
this.renderPromises = React.useContext(getApolloContext()).renderPromises;
234235

235236
this.useOptions(options);
236237

237238
const obsQuery = this.useObservableQuery();
238239

240+
// eslint-disable-next-line react-hooks/rules-of-hooks
239241
const result = useSyncExternalStore(
242+
// eslint-disable-next-line react-hooks/rules-of-hooks
240243
React.useCallback(
241244
(handleStoreChange) => {
242245
if (this.renderPromises) {
@@ -307,7 +310,9 @@ class InternalState<TData, TVariables extends OperationVariables> {
307310
// effectively passing this dependency array to that useEffect buried
308311
// inside useSyncExternalStore, as desired.
309312
obsQuery,
313+
// eslint-disable-next-line react-hooks/exhaustive-deps
310314
this.renderPromises,
315+
// eslint-disable-next-line react-hooks/exhaustive-deps
311316
this.client.disableNetworkFetches,
312317
]
313318
),
@@ -533,6 +538,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
533538
this.observable || // Reuse this.observable if possible (and not SSR)
534539
this.client.watchQuery(this.getObsQueryOptions()));
535540

541+
// eslint-disable-next-line react-hooks/rules-of-hooks
536542
this.obsQueryFields = React.useMemo(
537543
() => ({
538544
refetch: obsQuery.refetch.bind(obsQuery),

src/react/hooks/useQueryRefHandlers.ts

+4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ export function useQueryRefHandlers<
5959
// client that's available to us at the current position in the React tree
6060
// that ApolloClient will then have the job to recreate a real queryRef from
6161
// the transported object
62+
// This is just a context read - it's fine to do this conditionally.
63+
// This hook wrapper also shouldn't be optimized by React Compiler.
64+
// eslint-disable-next-line react-compiler/react-compiler
65+
// eslint-disable-next-line react-hooks/rules-of-hooks
6266
: useApolloClient()
6367
)(queryRef);
6468
}

src/react/hooks/useReadQuery.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ export function useReadQuery<TData>(
5252
// client that's available to us at the current position in the React tree
5353
// that ApolloClient will then have the job to recreate a real queryRef from
5454
// the transported object
55+
// This is just a context read - it's fine to do this conditionally.
56+
// This hook wrapper also shouldn't be optimized by React Compiler.
57+
// eslint-disable-next-line react-compiler/react-compiler
58+
// eslint-disable-next-line react-hooks/rules-of-hooks
5559
: useApolloClient()
5660
)(queryRef);
5761
}
@@ -85,7 +89,7 @@ function _useReadQuery<TData>(
8589
forceUpdate();
8690
});
8791
},
88-
[internalQueryRef]
92+
[internalQueryRef, queryRef]
8993
),
9094
getPromise,
9195
getPromise

src/react/hooks/useSubscription.ts

+4
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ export function useSubscription<
204204
}
205205

206206
Object.assign(ref.current, { client, subscription, options });
207+
// eslint-disable-next-line react-compiler/react-compiler
208+
// eslint-disable-next-line react-hooks/exhaustive-deps
207209
}, [client, subscription, options, canResetObservableRef.current]);
208210

209211
React.useEffect(() => {
@@ -271,6 +273,8 @@ export function useSubscription<
271273
subscription.unsubscribe();
272274
});
273275
};
276+
// eslint-disable-next-line react-compiler/react-compiler
277+
// eslint-disable-next-line react-hooks/exhaustive-deps
274278
}, [observable]);
275279

276280
return result;

src/react/hooks/useSuspenseQuery.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -251,18 +251,18 @@ function _useSuspenseQuery<
251251
}, [queryRef.result]);
252252

253253
const result = fetchPolicy === "standby" ? skipResult : __use(promise);
254-
const fetchMore = React.useCallback(
255-
((options) => {
254+
255+
const fetchMore = React.useCallback<
256+
FetchMoreFunction<unknown, OperationVariables>
257+
>(
258+
(options) => {
256259
const promise = queryRef.fetchMore(options);
257260
setPromise([queryRef.key, queryRef.promise]);
258261

259262
return promise;
260-
}) satisfies FetchMoreFunction<
261-
unknown,
262-
OperationVariables
263-
> as FetchMoreFunction<TData | undefined, TVariables>,
263+
},
264264
[queryRef]
265-
);
265+
) as FetchMoreFunction<TData | undefined, TVariables>;
266266

267267
const refetch: RefetchFunction<TData, TVariables> = React.useCallback(
268268
(variables) => {

src/react/hooks/useSyncExternalStore.ts

+4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export const useSyncExternalStore: RealUseSESHookType =
8181
// Force a re-render.
8282
forceUpdate({ inst });
8383
}
84+
// React Hook React.useLayoutEffect has a missing dependency: 'inst'. Either include it or remove the dependency array.
85+
// eslint-disable-next-line react-hooks/exhaustive-deps
8486
}, [subscribe, value, getSnapshot]);
8587
} else {
8688
Object.assign(inst, { value, getSnapshot });
@@ -108,6 +110,8 @@ export const useSyncExternalStore: RealUseSESHookType =
108110
forceUpdate({ inst });
109111
}
110112
});
113+
// React Hook React.useEffect has a missing dependency: 'inst'. Either include it or remove the dependency array.
114+
// eslint-disable-next-line react-hooks/exhaustive-deps
111115
}, [subscribe]);
112116

113117
return value;

0 commit comments

Comments
 (0)