Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enable react-hooks lint rules #11511

Merged
merged 24 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2d5f02f
enable `react-hooks` lint rules
phryneas Jan 22, 2024
913ea1f
Merge remote-tracking branch 'origin/main' into pr/lint-hooks
phryneas May 16, 2024
9e786aa
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 16, 2024
d757781
update eslint rule
phryneas May 16, 2024
0e5e3cb
different type casting to not break lint rule parsing
phryneas May 16, 2024
6ad1b0e
fix up useMutation + lifecycle test
phryneas May 16, 2024
172f26c
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 16, 2024
c5a5d28
avoid reading/writing ref in render
phryneas May 16, 2024
6070945
fix up useLazyQuery
phryneas May 16, 2024
9c78b5c
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 16, 2024
8a33815
fix up another test doing a mutation in render
phryneas May 17, 2024
1aeb9ff
more useQuery cleanup
phryneas May 17, 2024
c34a48c
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 17, 2024
34ef7d8
ignore rule of hook for context access
phryneas May 17, 2024
c5bb497
undo changes that were moved into separate PRs
phryneas May 17, 2024
8cdba15
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 17, 2024
2e751e1
almost full rewrite of the `useSubscription` hook
phryneas May 17, 2024
d70b45e
Clean up Prettier, Size-limit, and Api-Extractor
phryneas May 17, 2024
0c813a1
ignore any rule violation lint warnings in useSubscription and useQuery
phryneas May 22, 2024
e9c247b
changeset
phryneas May 22, 2024
619b188
rename patch
phryneas May 22, 2024
7743e0b
Merge remote-tracking branch 'origin/main' into pr/lint-hooks
phryneas May 27, 2024
694452f
also add "eslint-plugin-react-compiler" rules
phryneas May 27, 2024
9eaa883
Merge branch 'main' into pr/lint-hooks
phryneas Jun 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/famous-camels-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": patch
---

`useLoadableQuery`: ensure that `loadQuery` is updated if the ApolloClient instance changes
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
{
"files": ["**/*.ts", "**/*.tsx"],
"excludedFiles": ["**/__tests__/**/*.*", "*.d.ts"],
"extends": ["plugin:react-hooks/recommended"],
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["eslint-plugin-react-compiler"],
"rules": {
"react-compiler/react-compiler": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
Expand Down
875 changes: 660 additions & 215 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "npm:@phryneas/[email protected]",
"eslint-plugin-local-rules": "2.0.1",
"eslint-plugin-react-compiler": "0.0.0-experimental-c8b3f72-20240517",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-testing-library": "6.2.2",
"expect-type": "0.19.0",
"fetch-mock": "9.11.0",
Expand Down
28 changes: 28 additions & 0 deletions patches/eslint-plugin-react-hooks+4.6.2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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
index 441442f..d1ec5dc 100644
--- 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
@@ -905,7 +905,7 @@ var ExhaustiveDeps = {
var _callee = callee,
name = _callee.name;

- if (name === 'useRef' && id.type === 'Identifier') {
+ if ((name === 'useRef' || name === "useLazyRef") && id.type === 'Identifier') {
// useRef() return value is stable.
return true;
} else if (name === 'useState' || name === 'useReducer') {
diff --git a/node_modules/eslint-plugin-react-hooks/index.js b/node_modules/eslint-plugin-react-hooks/index.js
index 0e91baf..7e86d46 100644
--- a/node_modules/eslint-plugin-react-hooks/index.js
+++ b/node_modules/eslint-plugin-react-hooks/index.js
@@ -1,9 +1,3 @@
'use strict';

-// TODO: this doesn't make sense for an ESLint rule.
-// We need to fix our build process to not create bundles for "raw" packages like this.
-if (process.env.NODE_ENV === 'production') {
- module.exports = require('./cjs/eslint-plugin-react-hooks.production.min.js');
-} else {
- module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
-}
+module.exports = require('./cjs/eslint-plugin-react-hooks.development.js');
1 change: 1 addition & 0 deletions src/react/hooks/internal/useRenderGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Relay does this too, so we hope this is safe.
https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98
*/
export function useRenderGuard() {
// eslint-disable-next-line react-compiler/react-compiler
RenderDispatcher = getRenderDispatcher();

return React.useCallback(() => {
Expand Down
11 changes: 9 additions & 2 deletions src/react/hooks/useLoadableQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,19 @@ export function useLoadableQuery<

setQueryRef(wrapQueryRef(queryRef));
},
[query, queryKey, suspenseCache, watchQueryOptions, calledDuringRender]
[
query,
queryKey,
suspenseCache,
watchQueryOptions,
calledDuringRender,
client,
]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up until now, loadQuery would not follow a new client instance.

@jerelmiller this might be something we should still take into account for 3.9, or at least 3.9.1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! I'm ok with doing a fast follow with 3.9.1 here if we want to keep with the code freeze. Thanks for finding!

);

const reset: ResetFunction = React.useCallback(() => {
setQueryRef(null);
}, [queryRef]);
}, []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I wasn't careful enough in my own code review when we released this to catch this. Let's definitely get this change in a patch 🙂


return [loadQuery, queryRef, { fetchMore, refetch, reset }];
}
6 changes: 6 additions & 0 deletions src/react/hooks/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,13 +230,16 @@ class InternalState<TData, TVariables extends OperationVariables> {
// initialization, this.renderPromises is usually undefined (unless SSR is
// happening), but that's fine as long as it has been initialized that way,
// rather than left uninitialized.
// eslint-disable-next-line react-hooks/rules-of-hooks
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to refactor this whole hook setup to not use a class anymore, it's not only confusing me to no end, it also confuses the lint rules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please

this.renderPromises = React.useContext(getApolloContext()).renderPromises;

this.useOptions(options);

const obsQuery = this.useObservableQuery();

// eslint-disable-next-line react-hooks/rules-of-hooks
const result = useSyncExternalStore(
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useCallback(
(handleStoreChange) => {
if (this.renderPromises) {
Expand Down Expand Up @@ -307,7 +310,9 @@ class InternalState<TData, TVariables extends OperationVariables> {
// effectively passing this dependency array to that useEffect buried
// inside useSyncExternalStore, as desired.
obsQuery,
// eslint-disable-next-line react-hooks/exhaustive-deps
this.renderPromises,
// eslint-disable-next-line react-hooks/exhaustive-deps
this.client.disableNetworkFetches,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this.client.disableNetworkFetches is never referenced within the callback, so the lint rules suggest to remove it. I assume that that's a good thing - ObservableQuery doesn't have a concept of disableNetworkFetches (it's a client property) and unsubscribing/resubscribing will happen so fast that it doesn't trigger a new attempt to make a request anyways.

Am I missing anything here?

]
),
Expand Down Expand Up @@ -533,6 +538,7 @@ class InternalState<TData, TVariables extends OperationVariables> {
this.observable || // Reuse this.observable if possible (and not SSR)
this.client.watchQuery(this.getObsQueryOptions()));

// eslint-disable-next-line react-hooks/rules-of-hooks
this.obsQueryFields = React.useMemo(
() => ({
refetch: obsQuery.refetch.bind(obsQuery),
Expand Down
4 changes: 4 additions & 0 deletions src/react/hooks/useQueryRefHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export function useQueryRefHandlers<
// client that's available to us at the current position in the React tree
// that ApolloClient will then have the job to recreate a real queryRef from
// the transported object
// This is just a context read - it's fine to do this conditionally.
// This hook wrapper also shouldn't be optimized by React Compiler.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/rules-of-hooks
: useApolloClient()
)(queryRef);
}
Expand Down
6 changes: 5 additions & 1 deletion src/react/hooks/useReadQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export function useReadQuery<TData>(
// client that's available to us at the current position in the React tree
// that ApolloClient will then have the job to recreate a real queryRef from
// the transported object
// This is just a context read - it's fine to do this conditionally.
// This hook wrapper also shouldn't be optimized by React Compiler.
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/rules-of-hooks
: useApolloClient()
)(queryRef);
}
Expand Down Expand Up @@ -85,7 +89,7 @@ function _useReadQuery<TData>(
forceUpdate();
});
},
[internalQueryRef]
[internalQueryRef, queryRef]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If queryRef changed, internalQueryRef should have changed anyways, but it doesn't hurt to have it here to stick to the rules.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed

),
getPromise,
getPromise
Expand Down
4 changes: 4 additions & 0 deletions src/react/hooks/useSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export function useSubscription<
}

Object.assign(ref.current, { client, subscription, options });
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [client, subscription, options, canResetObservableRef.current]);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canResetObservableRef is a ref and it's current value should not be used to trigger an effect, as changing it doesn't trigger a rerender and the resulting effect would be postponed to some random point in time in the future.

This might be a change in behaviour though - we need to be careful here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL we might have a tendency to overuse refs 😅


React.useEffect(() => {
Expand Down Expand Up @@ -271,6 +273,8 @@ export function useSubscription<
subscription.unsubscribe();
});
};
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [observable]);

return result;
Expand Down
14 changes: 7 additions & 7 deletions src/react/hooks/useSuspenseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,18 +251,18 @@ function _useSuspenseQuery<
}, [queryRef.result]);

const result = fetchPolicy === "standby" ? skipResult : __use(promise);
const fetchMore = React.useCallback(
((options) => {

const fetchMore = React.useCallback<
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lint rule had massive problems parsing the satisfies ... as pattern, but this works.

FetchMoreFunction<unknown, OperationVariables>
>(
(options) => {
const promise = queryRef.fetchMore(options);
setPromise([queryRef.key, queryRef.promise]);

return promise;
}) satisfies FetchMoreFunction<
unknown,
OperationVariables
> as FetchMoreFunction<TData | undefined, TVariables>,
},
[queryRef]
);
) as FetchMoreFunction<TData | undefined, TVariables>;

const refetch: RefetchFunction<TData, TVariables> = React.useCallback(
(variables) => {
Expand Down
4 changes: 4 additions & 0 deletions src/react/hooks/useSyncExternalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const useSyncExternalStore: RealUseSESHookType =
// Force a re-render.
forceUpdate({ inst });
}
// React Hook React.useLayoutEffect has a missing dependency: 'inst'. Either include it or remove the dependency array.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh., I wouldn't touch our implementation of uSES for now and remove it completely in 4.0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. To be honest, I don't completely understand why we didn't just use the official polyfill to begin with, but it is what it is! Totally fine leaving this one as-is.

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe, value, getSnapshot]);
} else {
Object.assign(inst, { value, getSnapshot });
Expand Down Expand Up @@ -108,6 +110,8 @@ export const useSyncExternalStore: RealUseSESHookType =
forceUpdate({ inst });
}
});
// React Hook React.useEffect has a missing dependency: 'inst'. Either include it or remove the dependency array.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [subscribe]);

return value;
Expand Down
Loading