Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 6 additions & 6 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,14 @@ export default [{
'react-hooks/error-boundaries': ERROR,
'react-hooks/component-hook-factories': ERROR,
'react-hooks/gating': ERROR,
// 'react-hooks/globals': ERROR,
'react-hooks/globals': ERROR,
// 'react-hooks/immutability': ERROR,
// 'react-hooks/preserve-manual-memoization': ERROR,
// 'react-hooks/purity': ERROR,
// 'react-hooks/refs': ERROR,
// 'react-hooks/set-state-in-effect': 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/static-components': ERROR,
'react-hooks/unsupported-syntax': WARN,
'react-hooks/use-memo': ERROR,
'react-hooks/incompatible-library': WARN,
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
4 changes: 2 additions & 2 deletions packages/@react-aria/menu/src/useSafelyMouseToSubmenu.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import {RefObject} from '@react-types/shared';
import {useEffect, useRef, useState} from 'react';
import {useEffectEvent, useResizeObserver} from '@react-aria/utils';
import {useEffectEvent, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
import {useInteractionModality} from '@react-aria/interactions';

interface SafelyMouseToSubmenuOptions {
Expand Down Expand Up @@ -67,7 +67,7 @@ export function useSafelyMouseToSubmenu(options: SafelyMouseToSubmenuOptions): v
}
}, [menuRef, preventPointerEvents]);

useEffect(() => {
useLayoutEffect(() => {
let submenu = submenuRef.current;
let menu = menuRef.current;

Expand Down
4 changes: 2 additions & 2 deletions packages/@react-aria/toast/src/useToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {AriaButtonProps} from '@react-types/button';
import {AriaLabelingProps, DOMAttributes, FocusableElement, RefObject} from '@react-types/shared';
import {filterDOMProps, useId, useSlotId} from '@react-aria/utils';
import {filterDOMProps, useId, useLayoutEffect, useSlotId} from '@react-aria/utils';
// @ts-ignore
import intlMessages from '../intl/*.json';
import {QueuedToast, ToastState} from '@react-stately/toast';
Expand Down Expand Up @@ -68,7 +68,7 @@ export function useToast<T>(props: AriaToastProps<T>, state: ToastState<T>, ref:
// Originally was tied to animationStart/End via https://github.com/adobe/react-spectrum/pull/6223/commits/e22e319df64958e822ab7cd9685e96818cae9ba5
// but toasts don't always have animations.
let [isVisible, setIsVisible] = useState(false);
useEffect(() => {
useLayoutEffect(() => {
setIsVisible(true);
}, []);

Expand Down
23 changes: 16 additions & 7 deletions packages/@react-aria/utils/test/mergeRefs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import {mergeRefs} from '../';
import React, {useCallback, useRef} from 'react';
import React, {useCallback, useEffect, useRef} from 'react';
import {render} from '@react-spectrum/test-utils-internal';

describe('mergeRefs', () => {
Expand All @@ -20,16 +20,21 @@ describe('mergeRefs', () => {
let ref2;

const TextField = (props) => {
ref1 = useRef(null);
ref2 = useRef(null);
let internalRef1 = useRef(null);
let internalRef2 = useRef(null);
useEffect(() => {
ref1 = internalRef1;
ref2 = internalRef2;
}, [internalRef1, internalRef2]);

const ref = mergeRefs(ref1, ref2);
const ref = mergeRefs(internalRef1, internalRef2);
return <input {...props} ref={ref} />;
};

render(<TextField foo="foo" />);

expect(ref1.current).toBe(ref2.current);
expect(ref1.current).not.toBeNull();
});

if (parseInt(React.version.split('.')[0], 10) >= 19) {
Expand All @@ -40,14 +45,18 @@ describe('mergeRefs', () => {
let target = null;

const TextField = (props) => {
ref1 = useRef(null);
ref2 = useRef(null);
let internalRef1 = useRef(null);
let internalRef2 = useRef(null);
useEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

other option i guess would be to exclude the compiler from running on test files, but seems better to always be in good habits

ref1 = internalRef1;
ref2 = internalRef2;
}, [internalRef1, internalRef2]);
let ref3 = useCallback((node) => {
target = node;
return cleanUp;
}, []);

const ref = mergeRefs(ref1, ref2, ref3);
const ref = mergeRefs(internalRef1, internalRef2, ref3);
return <input {...props} ref={ref} />;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@ function ForwardSearchAutocompleteInput<T>(props: SearchAutocompleteInputProps<T
}
} else if (!isLoading) {
// If loading is no longer happening, clear any timers and hide the loading circle
setShowLoading(false);
if (timeout.current != null) {
clearTimeout(timeout.current);
timeout.current = null;
Expand All @@ -297,6 +296,11 @@ function ForwardSearchAutocompleteInput<T>(props: SearchAutocompleteInputProps<T

lastInputValue.current = inputValue;
}, [isLoading, showLoading, inputValue]);
let [prevIsLoading, setPrevIsLoading] = useState(isLoading);
if (prevIsLoading !== isLoading && !isLoading) {
setShowLoading(false);
setPrevIsLoading(isLoading);
}
Copy link
Member

Choose a reason for hiding this comment

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

?

Copy link
Member

Choose a reason for hiding this comment

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

yeah this one felt weird to me as well, but https://react.dev/reference/react/useState#storing-information-from-previous-renders claims it is better than setting state in an effect (which the linter will now complain about)

Copy link
Member Author

@snowystinger snowystinger Oct 23, 2025

Choose a reason for hiding this comment

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

this one is very annoying, but I'm not sure a better way to do it, we "react" to the change in the prop value, which means we set state in render

we use the showLoading state and count on the new render as a response to the timeout up above, so we need to have some way to "un set it"

had to do it in Combobox as well


return (
(<FocusRing
Expand Down
13 changes: 6 additions & 7 deletions packages/@react-spectrum/breadcrumbs/test/Breadcrumbs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {act, pointerMap, render, within} from '@react-spectrum/test-utils-intern
import {Breadcrumbs} from '../';
import {Item} from '@react-stately/collections';
import {Provider} from '@react-spectrum/provider';
import React, {useRef} from 'react';
import React, {createRef, forwardRef} from 'react';
import {theme} from '@react-spectrum/theme-default';
import userEvent from '@testing-library/user-event';

Expand Down Expand Up @@ -93,16 +93,15 @@ describe('Breadcrumbs', function () {
});

it('Should handle forward ref', function () {
let ref;
let Component = () => {
ref = useRef();
let ref = createRef();
let Component = forwardRef((props, forwardedRef) => {
return (
<Breadcrumbs ref={ref} aria-label="breadcrumbs-test">
<Breadcrumbs ref={forwardedRef} aria-label="breadcrumbs-test">
<Item>Folder 1</Item>
</Breadcrumbs>
);
};
let {getByLabelText} = render(<Component />);
});
let {getByLabelText} = render(<Component ref={ref} />);
let breadcrumb = getByLabelText('breadcrumbs-test');
expect(breadcrumb).toBe(ref.current.UNSAFE_getDOMNode());
});
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/color/src/ColorWheel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import {classNames, dimensionValue, useFocusableRef, useStyleProps} from '@react
import {ColorThumb} from './ColorThumb';
import {ColorWheelContext, useContextProps} from 'react-aria-components';
import {FocusableRef} from '@react-types/shared';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import React, {useCallback, useRef, useState} from 'react';
import {SpectrumColorWheelProps} from '@react-types/color';
import styles from '@adobe/spectrum-css-temp/components/colorwheel/vars.css';
import {useColorWheel} from '@react-aria/color';
import {useColorWheelState} from '@react-stately/color';
import {useFocusRing} from '@react-aria/focus';
import {useLayoutEffect, useResizeObserver} from '@react-aria/utils';
import {useProviderProps} from '@react-spectrum/provider';
import {useResizeObserver} from '@react-aria/utils';

const WHEEL_THICKNESS = 24;

Expand Down Expand Up @@ -53,7 +53,7 @@ export const ColorWheel = React.forwardRef(function ColorWheel(props: SpectrumCo
}
}, [containerRef, setWheelRadius, setWheelThickness]);

useEffect(() => {
useLayoutEffect(() => {
// the size observer's fallback to the window resize event doesn't fire on mount
if (wheelRadius === 0) {
resizeHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ export const Default: ColorSwatchPickerStory = (args) => (
</ColorSwatchPicker>
);

let randomColors = Array.from(Array(24)).map(() => {
return `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')}`;
});
export const ManySwatches: ColorSwatchPickerStory = (args) => (
<ColorSwatchPicker {...args} maxWidth="size-3000">
{Array.from(Array(24)).map(() => {
let color = `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')}`;
{randomColors.map((color) => {
return <ColorSwatch key={color} color={color} />;
})}
</ColorSwatchPicker>
Expand Down
8 changes: 6 additions & 2 deletions packages/@react-spectrum/combobox/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,6 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
}, 500);
}
} else if (!isLoading) {
// If loading is no longer happening, clear any timers and hide the loading circle
setShowLoading(false);
if (timeout.current) {
clearTimeout(timeout.current);
}
Expand All @@ -289,6 +287,12 @@ const ComboBoxInput = React.forwardRef(function ComboBoxInput(props: ComboBoxInp
lastInputValue.current = inputValue;
}, [isLoading, showLoading, inputValue]);

let [prevIsLoading, setPrevIsLoading] = useState(isLoading);
if (prevIsLoading !== isLoading && !isLoading) {
setShowLoading(false);
setPrevIsLoading(isLoading);
}

useEffect(() => {
return () => {
if (timeout.current) {
Expand Down
10 changes: 7 additions & 3 deletions packages/@react-spectrum/list/stories/ListViewDnDExamples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,15 @@ let itemList2 = [
{id: '12', type: 'item', textValue: 'Item Twelve'}
];

let randomDragTypeReorderExample = `keys-${Math.random().toString(36).slice(2)}`;
export function ReorderExample(props: SpectrumListViewProps<any> & DragAndDropOptions & {getAllowedDropOperationsAction?: () => void}): JSX.Element {
let {items, onDrop, onDragStart, onDragEnd, disabledKeys = ['2'], ...otherprops} = props;
let list = useListData({
initialItems: items ? [...items] : itemList1
});

// 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, []);

let onMove = (keys: Key[], target: ItemDropTarget) => {
if (target.dropPosition === 'before') {
Expand Down Expand Up @@ -166,6 +167,7 @@ export function ReorderExample(props: SpectrumListViewProps<any> & DragAndDropOp
);
}

let randomDragTypeDragIntoItemExample = `keys-${Math.random().toString(36).slice(2)}`;
export function DragIntoItemExample(props: {listViewProps: SpectrumListViewProps<any>, dragHookOptions?: DragAndDropOptions, dropHookOptions?: DragAndDropOptions, getAllowedDropOperationsAction?: () => void}): JSX.Element {
let {
listViewProps = {},
Expand Down Expand Up @@ -193,7 +195,7 @@ export function DragIntoItemExample(props: {listViewProps: SpectrumListViewProps
let disabledKeys: Key[] = ['2', '7'];

// 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(() => randomDragTypeDragIntoItemExample, []);

let onMove = (keys: Key[], target: ItemDropTarget) => {
let folderItem = list.getItem(target.key)!;
Expand Down Expand Up @@ -274,6 +276,8 @@ export function DragIntoItemExample(props: {listViewProps: SpectrumListViewProps
);
}

let randomDragTypeDragBetweenListsExample = `keys-${Math.random().toString(36).slice(2)}`;

export function DragBetweenListsExample(props: SpectrumListViewProps<any> & DragAndDropOptions & {getAllowedDropOperationsAction?: () => void, items1?: any[], items2?: any[]}): JSX.Element {
let {onDragStart, onDragEnd, onDrop} = props;
let onDropAction = chain(action('onDrop'), onDrop);
Expand Down Expand Up @@ -311,7 +315,7 @@ export function DragBetweenListsExample(props: SpectrumListViewProps<any> & Drag
};

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

let {dragAndDropHooks} = useDragAndDrop({
getItems(keys) {
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/menu/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ export const Menu = React.forwardRef(function Menu<T extends object>(props: Spec
useSyncRef(contextProps, domRef);
let [leftOffset, setLeftOffset] = useState({left: 0});
let prevPopoverContainer = useRef<HTMLElement | null>(null);
useEffect(() => {

useLayoutEffect(() => {
if (popoverContainer && prevPopoverContainer.current !== popoverContainer && leftOffset.left === 0) {
prevPopoverContainer.current = popoverContainer;
let {left} = popoverContainer.getBoundingClientRect();
Expand Down
Loading