Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5d3e2f3
chore: update eslint plugin react hooks
snowystinger Oct 10, 2025
7b96414
fix all lint errors
snowystinger Oct 10, 2025
7f79dd9
Add lines for other config items
snowystinger Oct 13, 2025
1208e74
Revert "fix all lint errors"
snowystinger Oct 13, 2025
3f9b6c9
fix all rules of hooks and exhaustive dependencies
snowystinger Oct 14, 2025
116fe27
enable all rules we are already passing
snowystinger Oct 14, 2025
a7d4e7e
fix event order and cleanup
snowystinger Oct 15, 2025
30bc1fa
fix lint
snowystinger Oct 15, 2025
5a45466
Merge branch 'main' into update-eslint-plugin-react-hooks
snowystinger Oct 15, 2025
ba77f40
turn on and fix static-components
snowystinger Oct 15, 2025
8f1cd8e
turn on and fix set-state-in-effect
snowystinger Oct 15, 2025
95687e9
turn on and fix purity
snowystinger Oct 15, 2025
a5803c0
fix lower react version tests
snowystinger Oct 15, 2025
be90742
turn on globals and fix errors
snowystinger Oct 15, 2025
4cd669d
Merge branch 'main' into update-eslint-plugin-react-hooks
snowystinger Oct 17, 2025
c073772
Merge branch 'update-eslint-plugin-react-hooks' into update-eslint-pl…
snowystinger Oct 17, 2025
2f591d5
move event listener attachment to an effect
snowystinger Oct 19, 2025
500c564
Merge branch 'update-eslint-plugin-react-hooks' into update-eslint-pl…
snowystinger Oct 19, 2025
a81fa41
convert remaining useElementTypes
snowystinger Oct 19, 2025
bad9964
Merge branch 'main' into update-eslint-plugin-react-hooks
snowystinger Oct 23, 2025
e88adfa
Merge branch 'update-eslint-plugin-react-hooks' into update-eslint-pl…
snowystinger Oct 23, 2025
fe3d7b5
fix read from ref in render
snowystinger Oct 23, 2025
37dee08
Merge branch 'main' into update-eslint-plugin-react-hooks-followup
snowystinger Oct 24, 2025
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
25 changes: 22 additions & 3 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import reactHooks from "eslint-plugin-react-hooks";
import jest from "eslint-plugin-jest";
import monorepo from "@jdb8/eslint-plugin-monorepo";
import * as rspRules from "eslint-plugin-rsp-rules";
import { fixupPluginRules } from "@eslint/compat";
import globals from "globals";
import babelParser from "@babel/eslint-parser";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
Expand Down Expand Up @@ -67,7 +66,7 @@ export default [{
react,
rulesdir,
"jsx-a11y": jsxA11Y,
"react-hooks": fixupPluginRules(reactHooks),
"react-hooks": reactHooks,
jest,
monorepo,
"rsp-rules": rspRules,
Expand Down Expand Up @@ -225,8 +224,28 @@ export default [{
"react/jsx-boolean-value": ERROR,
"react/jsx-first-prop-new-line": [ERROR, "multiline"],
"react/self-closing-comp": ERROR,

// Core hooks rules
"react-hooks/rules-of-hooks": ERROR, // https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/CHANGELOG.md
"react-hooks/exhaustive-deps": WARN,

// React Compiler rules
'react-hooks/config': ERROR,
'react-hooks/error-boundaries': ERROR,
'react-hooks/component-hook-factories': ERROR,
'react-hooks/gating': ERROR,
'react-hooks/globals': ERROR,
// 'react-hooks/immutability': ERROR,
// 'react-hooks/preserve-manual-memoization': ERROR, // No idea how to turn this one on yet
'react-hooks/purity': ERROR,
// 'react-hooks/refs': ERROR, // can't turn on until https://github.com/facebook/react/issues/34775 is fixed
'react-hooks/set-state-in-effect': ERROR,
'react-hooks/set-state-in-render': ERROR,
'react-hooks/static-components': ERROR,
'react-hooks/unsupported-syntax': WARN,
'react-hooks/use-memo': ERROR,
'react-hooks/incompatible-library': WARN,

"rsp-rules/no-react-key": [ERROR],
"rsp-rules/sort-imports": [ERROR],
"rulesdir/imports": [ERROR],
Expand Down Expand Up @@ -332,7 +351,7 @@ export default [{
react,
rulesdir,
"jsx-a11y": jsxA11Y,
"react-hooks": fixupPluginRules(reactHooks),
"react-hooks": reactHooks,
jest,
"@typescript-eslint": typescriptEslint,
monorepo,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
"eslint-plugin-jsdoc": "^50.4.1",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-rulesdir": "^0.2.2",
"fast-check": "^2.19.0",
"fast-glob": "^3.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/actiongroup/src/useActionGroupItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function useActionGroupItem<T>(props: AriaActionGroupItemProps, state: Li
return () => {
onRemovedWithFocus();
};
}, [onRemovedWithFocus]);
}, []);

return {
buttonProps: mergeProps(buttonProps, {
Expand Down
14 changes: 7 additions & 7 deletions packages/@react-aria/autocomplete/src/useAutocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
return () => clearTimeout(timeout.current);
}, []);

let updateActiveDescendant = useEffectEvent((e: Event) => {
let updateActiveDescendant = useCallback((e: Event) => {
// Ensure input is focused if the user clicks on the collection directly.
if (!e.isTrusted && shouldUseVirtualFocus && inputRef.current && getActiveElement(getOwnerDocument(inputRef.current)) !== inputRef.current) {
inputRef.current.focus();
Expand Down Expand Up @@ -138,7 +138,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
}

delayNextActiveDescendant.current = false;
});
}, [shouldUseVirtualFocus, inputRef, collectionRef, state]);

let callbackRef = useCallback((collectionNode) => {
if (collectionNode != null) {
Expand All @@ -165,7 +165,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
// Make sure to memo so that React doesn't keep registering a new event listeners on every rerender of the wrapped collection
let mergedCollectionRef = useObjectRef(useMemo(() => mergeRefs(collectionRef, callbackRef), [collectionRef, callbackRef]));

let focusFirstItem = useEffectEvent(() => {
let focusFirstItem = useCallback(() => {
delayNextActiveDescendant.current = true;
collectionRef.current?.dispatchEvent(
new CustomEvent(FOCUS_EVENT, {
Expand All @@ -176,9 +176,9 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
}
})
);
});
}, [collectionRef]);

let clearVirtualFocus = useEffectEvent((clearFocusKey?: boolean) => {
let clearVirtualFocus = useCallback((clearFocusKey?: boolean) => {
moveVirtualFocus(getActiveElement());
queuedActiveDescendant.current = null;
state.setFocusedNodeId(null);
Expand All @@ -192,7 +192,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
clearTimeout(timeout.current);
delayNextActiveDescendant.current = false;
collectionRef.current?.dispatchEvent(clearFocusEvent);
});
}, [collectionRef, state]);

let lastInputType = useRef('');
useEvent(inputRef, 'input', e => {
Expand Down Expand Up @@ -346,7 +346,7 @@ export function useAutocomplete<T>(props: AriaAutocompleteOptions<T>, state: Aut
return () => {
document.removeEventListener('keyup', onKeyUpCapture, true);
};
}, [onKeyUpCapture]);
}, []);

let stringFormatter = useLocalizedStringFormatter(intlMessages, '@react-aria/autocomplete');
let collectionProps = useLabels({
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/dnd/src/useClipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export function useClipboard(options: ClipboardProps): ClipboardResult {
addGlobalEventListener('beforepaste', onBeforePaste),
addGlobalEventListener('paste', onPaste)
);
}, [isDisabled, onBeforeCopy, onCopy, onBeforeCut, onCut, onBeforePaste, onPaste]);
}, [isDisabled]);

return {
clipboardProps: focusProps
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/dnd/src/useDrop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export function useDrop(options: DropOptions): DropResult {
onDrop: onKeyboardDrop,
onDropActivate
});
}, [isDisabled, ref, getDropOperationKeyboard, onDropEnter, onDropExit, onKeyboardDrop, onDropActivate]);
}, [isDisabled, ref]);

let {dropProps} = useVirtualDrop();
if (isDisabled) {
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-aria/dnd/stories/Reorderable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function ReorderableGridExample(props: any): JSX.Element {
);
}

let randomDragTypeReorderExample = `keys-${Math.random().toString(36).slice(2)}`;
function ReorderableGrid(props) {
let ref = React.useRef<HTMLDivElement>(null);
let state = useListState(props);
Expand All @@ -91,7 +92,7 @@ function ReorderableGrid(props) {
});

// Use a random drag type so the items can only be reordered within this list and not dragged elsewhere.
let dragType = React.useMemo(() => `keys-${Math.random().toString(36).slice(2)}`, []);
let dragType = React.useMemo(() => randomDragTypeReorderExample, []);
Copy link
Member Author

Choose a reason for hiding this comment

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

there's a lot of these, i just moved the random generator out of the component, that should be fine because you couldn't drag between different examples this way either and I don't think we render the same one twice as an example

let preview = useRef(null);
let dragState = useDraggableCollectionState({
collection: gridState.collection,
Expand Down
14 changes: 9 additions & 5 deletions packages/@react-aria/dnd/test/dnd.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {CUSTOM_DRAG_TYPE} from '../src/constants';
import {DataTransfer, DataTransferItem, DragEvent, FileSystemDirectoryEntry, FileSystemFileEntry} from './mocks';
import {Draggable, Droppable} from './examples';
import {DragTypes} from '../src/utils';
import React from 'react';
import React, {useEffect} from 'react';
import userEvent from '@testing-library/user-event';

function pointerEvent(type, opts) {
Expand Down Expand Up @@ -195,13 +195,13 @@ describe('useDrag and useDrop', function () {
let draggable = tree.getByText('Drag me');
let droppable = tree.getByText('Drop here');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

let dataTransfer = new DataTransfer();
fireEvent(draggable, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0}));
act(() => jest.runAllTimers());
expect(draggable).toHaveAttribute('data-dragging', 'true');
expect(droppable).toHaveAttribute('data-droptarget', 'false');

expect(onDragStart).toHaveBeenCalledTimes(1);
expect(onDragMove).not.toHaveBeenCalled();
expect(onDragEnd).not.toHaveBeenCalled();
Expand Down Expand Up @@ -2574,7 +2574,9 @@ describe('useDrag and useDrop', function () {
let setShowTarget2;
let Test = () => {
let [showTarget2, _setShowTarget2] = React.useState(false);
setShowTarget2 = _setShowTarget2;
useEffect(() => {
setShowTarget2 = _setShowTarget2;
}, [_setShowTarget2]);
return (<>
<Draggable />
<Droppable />
Expand Down Expand Up @@ -2635,7 +2637,9 @@ describe('useDrag and useDrop', function () {
let setShowTarget2;
let Test = () => {
let [showTarget2, _setShowTarget2] = React.useState(true);
setShowTarget2 = _setShowTarget2;
useEffect(() => {
setShowTarget2 = _setShowTarget2;
}, [_setShowTarget2]);
return (<>
<Draggable />
<Droppable />
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-aria/form/src/useFormValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export function useFormValidation<T>(props: FormValidationProps<T>, state: FormV
form.reset = reset;
}
};
}, [ref, onInvalid, onChange, onReset, validationBehavior]);
}, [ref, validationBehavior]);
}

function getValidity(input: ValidatableElement) {
Expand Down
20 changes: 15 additions & 5 deletions packages/@react-aria/grid/src/useGridSelectionAnnouncement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {Collection, Key, Node, Selection} from '@react-types/shared';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {SelectionManager} from '@react-stately/selection';
import {useEffectEvent, useUpdateEffect} from '@react-aria/utils';
import {useCallback, useRef} from 'react';
import {useLocalizedStringFormatter} from '@react-aria/i18n';
import {useRef} from 'react';
import {useUpdateEffect} from '@react-aria/utils';

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

Expand Down Expand Up @@ -101,8 +101,18 @@ export function useGridSelectionAnnouncement<T>(props: GridSelectionAnnouncement
}

lastSelection.current = selection;
});

}, [
selection,
state.selectionManager.selectedKeys,
state.selectionManager.isFocused,
state.selectionManager.selectionBehavior,
state.selectionManager.selectionMode,
state.collection,
getRowText,
stringFormatter
]);

// useUpdateEffect will handle using useEffectEvent, no need to stabilize anything on this end
useUpdateEffect(() => {
if (state.selectionManager.isFocused) {
announceSelectionChange();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function useInteractOutside(props: InteractOutsideProps): void {
documentObject.removeEventListener('touchend', onTouchEnd, true);
};
}
}, [ref, isDisabled, onPointerDown, triggerInteractOutside]);
}, [ref, isDisabled]);
}

function isValidEvent(event, ref) {
Expand Down
Loading
Loading