Skip to content

Commit a0d9474

Browse files
authored
chore: Update eslint plugin react hooks (#9012)
* chore: update eslint plugin react hooks * fix all lint errors * Add lines for other config items * Revert "fix all lint errors" This reverts commit 7b96414. * fix all rules of hooks and exhaustive dependencies * enable all rules we are already passing * fix event order and cleanup * fix lint * move event listener attachment to an effect
1 parent 0d94d89 commit a0d9474

36 files changed

+554
-385
lines changed

eslint.config.mjs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import reactHooks from "eslint-plugin-react-hooks";
55
import jest from "eslint-plugin-jest";
66
import monorepo from "@jdb8/eslint-plugin-monorepo";
77
import * as rspRules from "eslint-plugin-rsp-rules";
8-
import { fixupPluginRules } from "@eslint/compat";
98
import globals from "globals";
109
import babelParser from "@babel/eslint-parser";
1110
import typescriptEslint from "@typescript-eslint/eslint-plugin";
@@ -67,7 +66,7 @@ export default [{
6766
react,
6867
rulesdir,
6968
"jsx-a11y": jsxA11Y,
70-
"react-hooks": fixupPluginRules(reactHooks),
69+
"react-hooks": reactHooks,
7170
jest,
7271
monorepo,
7372
"rsp-rules": rspRules,
@@ -225,8 +224,28 @@ export default [{
225224
"react/jsx-boolean-value": ERROR,
226225
"react/jsx-first-prop-new-line": [ERROR, "multiline"],
227226
"react/self-closing-comp": ERROR,
227+
228+
// Core hooks rules
228229
"react-hooks/rules-of-hooks": ERROR, // https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md
229230
"react-hooks/exhaustive-deps": WARN,
231+
232+
// React Compiler rules
233+
'react-hooks/config': ERROR,
234+
'react-hooks/error-boundaries': ERROR,
235+
'react-hooks/component-hook-factories': ERROR,
236+
'react-hooks/gating': ERROR,
237+
// 'react-hooks/globals': ERROR,
238+
// 'react-hooks/immutability': ERROR,
239+
// 'react-hooks/preserve-manual-memoization': ERROR,
240+
// 'react-hooks/purity': ERROR,
241+
// 'react-hooks/refs': ERROR,
242+
// 'react-hooks/set-state-in-effect': ERROR,
243+
'react-hooks/set-state-in-render': ERROR,
244+
// 'react-hooks/static-components': ERROR,
245+
'react-hooks/unsupported-syntax': WARN,
246+
'react-hooks/use-memo': ERROR,
247+
'react-hooks/incompatible-library': WARN,
248+
230249
"rsp-rules/no-react-key": [ERROR],
231250
"rsp-rules/sort-imports": [ERROR],
232251
"rulesdir/imports": [ERROR],
@@ -332,7 +351,7 @@ export default [{
332351
react,
333352
rulesdir,
334353
"jsx-a11y": jsxA11Y,
335-
"react-hooks": fixupPluginRules(reactHooks),
354+
"react-hooks": reactHooks,
336355
jest,
337356
"@typescript-eslint": typescriptEslint,
338357
monorepo,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@
151151
"eslint-plugin-jsdoc": "^50.4.1",
152152
"eslint-plugin-jsx-a11y": "^6.10.0",
153153
"eslint-plugin-react": "^7.37.1",
154-
"eslint-plugin-react-hooks": "^5.0.0",
154+
"eslint-plugin-react-hooks": "^7.0.0",
155155
"eslint-plugin-rulesdir": "^0.2.2",
156156
"fast-check": "^2.19.0",
157157
"fast-glob": "^3.1.0",

packages/@react-aria/actiongroup/src/useActionGroupItem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export function useActionGroupItem<T>(props: AriaActionGroupItemProps, state: Li
5454
return () => {
5555
onRemovedWithFocus();
5656
};
57-
}, [onRemovedWithFocus]);
57+
}, []);
5858

5959
return {
6060
buttonProps: mergeProps(buttonProps, {

packages/@react-aria/autocomplete/src/useAutocomplete.ts

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import {AriaLabelingProps, BaseEvent, DOMProps, FocusableElement, FocusEvents, KeyboardEvents, Node, RefObject, ValueBase} from '@react-types/shared';
1414
import {AriaTextFieldProps} from '@react-aria/textfield';
1515
import {AutocompleteProps, AutocompleteState} from '@react-stately/autocomplete';
16-
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useObjectRef} from '@react-aria/utils';
16+
import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, getActiveElement, getOwnerDocument, isAndroid, isCtrlKeyPressed, isIOS, mergeProps, mergeRefs, useEffectEvent, useEvent, useId, useLabels, useLayoutEffect, useObjectRef} from '@react-aria/utils';
1717
import {dispatchVirtualBlur, dispatchVirtualFocus, getVirtuallyFocusedElement, moveVirtualFocus} from '@react-aria/focus';
1818
import {getInteractionModality} from '@react-aria/interactions';
1919
// @ts-ignore
@@ -92,7 +92,6 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
9292
let timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
9393
let delayNextActiveDescendant = useRef(false);
9494
let queuedActiveDescendant = useRef<string | null>(null);
95-
let lastCollectionNode = useRef<HTMLElement>(null);
9695

9796
// For mobile screen readers, we don't want virtual focus, instead opting to disable FocusScope's restoreFocus and manually
9897
// moving focus back to the subtriggers
@@ -106,7 +105,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
106105
return () => clearTimeout(timeout.current);
107106
}, []);
108107

109-
let updateActiveDescendant = useEffectEvent((e: Event) => {
108+
let updateActiveDescendantEvent = useEffectEvent((e: Event) => {
110109
// Ensure input is focused if the user clicks on the collection directly.
111110
if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
112111
inputRef.current.focus();
@@ -140,32 +139,36 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
140139
delayNextActiveDescendant.current = false;
141140
});
142141

143-
let callbackRef = useCallback((collectionNode) => {
144-
if (collectionNode != null) {
145-
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
146-
// of the letter you just typed. If we recieve another focus event then we clear the queued update
147-
// We track lastCollectionNode to do proper cleanup since callbackRefs just pass null when unmounting. This also handles
148-
// React 19's extra call of the callback ref in strict mode
149-
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
150-
lastCollectionNode.current = collectionNode;
151-
collectionNode.addEventListener('focusin', updateActiveDescendant);
142+
let [collectionNode, setCollectionNode] = useState<HTMLElement | null>(null);
143+
let callbackRef = useCallback((node) => {
144+
setCollectionNode(node);
145+
if (node != null) {
152146
// If useSelectableCollection isn't passed shouldUseVirtualFocus even when useAutocomplete provides it
153147
// that means the collection doesn't support it (e.g. Table). If that is the case, we need to disable it here regardless
154148
// of what the user's provided so that the input doesn't recieve the onKeyDown and autocomplete props.
155-
if (collectionNode.getAttribute('tabindex') != null) {
149+
if (node.getAttribute('tabindex') != null) {
156150
setShouldUseVirtualFocus(false);
157151
}
158152
setHasCollection(true);
159153
} else {
160-
lastCollectionNode.current?.removeEventListener('focusin', updateActiveDescendant);
161154
setHasCollection(false);
162155
}
163-
}, [updateActiveDescendant]);
156+
}, []);
157+
useLayoutEffect(() => {
158+
if (collectionNode != null) {
159+
// When typing forward, we want to delay the setting of active descendant to not interrupt the native screen reader announcement
160+
// of the letter you just typed. If we recieve another focus event then we clear the queued update
161+
collectionNode.addEventListener('focusin', updateActiveDescendantEvent);
162+
}
163+
return () => {
164+
collectionNode?.removeEventListener('focusin', updateActiveDescendantEvent);
165+
};
166+
}, [collectionNode]);
164167

165168
// Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
166169
let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef]));
167170

168-
let focusFirstItem = useEffectEvent(() => {
171+
let focusFirstItem = useCallback(() => {
169172
delayNextActiveDescendant.current = true;
170173
collectionRef.current?.dispatchEvent(
171174
new CustomEvent(FOCUS_EVENT, {
@@ -176,9 +179,9 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
176179
}
177180
})
178181
);
179-
});
182+
}, [collectionRef]);
180183

181-
let clearVirtualFocus = useEffectEvent((clearFocusKey?: boolean) => {
184+
let clearVirtualFocus = useCallback((clearFocusKey?: boolean) => {
182185
moveVirtualFocus(getActiveElement());
183186
queuedActiveDescendant.current = null;
184187
state.setFocusedNodeId(null);
@@ -192,7 +195,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
192195
clearTimeout(timeout.current);
193196
delayNextActiveDescendant.current = false;
194197
collectionRef.current?.dispatchEvent(clearFocusEvent);
195-
});
198+
}, [collectionRef, state]);
196199

197200
let lastInputType = useRef('');
198201
useEvent(inputRef, 'input', e => {
@@ -346,7 +349,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
346349
return () => {
347350
document.removeEventListener('keyup', onKeyUpCapture, true);
348351
};
349-
}, [onKeyUpCapture]);
352+
}, []);
350353

351354
let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete');
352355
let collectionProps = useLabels({

packages/@react-aria/dnd/src/useClipboard.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export function useClipboard(options: ClipboardProps): ClipboardResult {
143143
addGlobalEventListener('beforepaste', onBeforePaste),
144144
addGlobalEventListener('paste', onPaste)
145145
);
146-
}, [isDisabled, onBeforeCopy, onCopy, onBeforeCut, onCut, onBeforePaste, onPaste]);
146+
}, [isDisabled]);
147147

148148
return {
149149
clipboardProps: focusProps

packages/@react-aria/dnd/src/useDrop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ export function useDrop(options: DropOptions): DropResult {
339339
onDrop: onKeyboardDrop,
340340
onDropActivate
341341
});
342-
}, [isDisabled, ref, getDropOperationKeyboard, onDropEnter, onDropExit, onKeyboardDrop, onDropActivate]);
342+
}, [isDisabled, ref]);
343343

344344
let {dropProps} = useVirtualDrop();
345345
if (isDisabled) {

packages/@react-aria/form/src/useFormValidation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
112112
form.reset = reset;
113113
}
114114
};
115-
}, [ref, onInvalid, onChange, onReset, validationBehavior]);
115+
}, [ref, validationBehavior]);
116116
}
117117

118118
function getValidity(input: ValidatableElement) {

packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {Collection, Key, Node, Selection} from '@react-types/shared';
1515
// @ts-ignore
1616
import intlMessages from '../intl/*.json';
1717
import {SelectionManager} from '@react-stately/selection';
18-
import {useEffectEvent, useUpdateEffect} from '@react-aria/utils';
18+
import {useCallback, useRef} from 'react';
1919
import {useLocalizedStringFormatter} from '@react-aria/i18n';
20-
import {useRef} from 'react';
20+
import {useUpdateEffect} from '@react-aria/utils';
2121

2222
export interface GridSelectionAnnouncementProps {
2323
/**
@@ -46,7 +46,7 @@ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncement
4646
// We do this using an ARIA live region.
4747
let selection = state.selectionManager.rawSelection;
4848
let lastSelection = useRef(selection);
49-
let announceSelectionChange = useEffectEvent(() => {
49+
let announceSelectionChange = useCallback(() => {
5050
if (!state.selectionManager.isFocused || selection === lastSelection.current) {
5151
lastSelection.current = selection;
5252

@@ -101,8 +101,18 @@ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncement
101101
}
102102

103103
lastSelection.current = selection;
104-
});
105-
104+
}, [
105+
selection,
106+
state.selectionManager.selectedKeys,
107+
state.selectionManager.isFocused,
108+
state.selectionManager.selectionBehavior,
109+
state.selectionManager.selectionMode,
110+
state.collection,
111+
getRowText,
112+
stringFormatter
113+
]);
114+
115+
// useUpdateEffect will handle using useEffectEvent, no need to stabilize anything on this end
106116
useUpdateEffect(() => {
107117
if (state.selectionManager.isFocused) {
108118
announceSelectionChange();

packages/@react-aria/interactions/src/useInteractOutside.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function useInteractOutside(props: InteractOutsideProps): void {
111111
documentObject.removeEventListener('touchend', onTouchEnd, true);
112112
};
113113
}
114-
}, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
114+
}, [ref, isDisabled]);
115115
}
116116

117117
function isValidEvent(event, ref) {

0 commit comments

Comments
 (0)