Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import storybook from 'eslint-plugin-storybook'
import react from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefreshPlugin from 'eslint-plugin-react-refresh'
import reactYouMightNotNeedAnEffect from 'eslint-plugin-react-you-might-not-need-an-effect'
import {unsupportedPatterns as reactCompilerUnsupported} from './packages/react/script/react-compiler.mjs'
import playwright from 'eslint-plugin-playwright'
import prettierRecommended from 'eslint-plugin-prettier/recommended'
Expand Down Expand Up @@ -115,6 +116,9 @@ const config = defineConfig([
},
},

// eslint-plugin-react-you-might-not-need-an-effect
reactYouMightNotNeedAnEffect.configs.recommended,

{
extends: fixupConfigRules(compat.extends('plugin:clsx/recommended', 'plugin:ssr-friendly/recommended')),
},
Expand Down
32 changes: 26 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"eslint-plugin-ssr-friendly": "1.3.0",
"eslint-plugin-storybook": "^10.4.2",
"eslint-plugin-testing-library": "^7.16.0",
"eslint-plugin-react-you-might-not-need-an-effect": "1.0.1",
"fast-glob": "^3.3.3",
"globals": "^16.2.0",
"markdownlint-cli2": "^0.19.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/ActionList/Description.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ export const Description: FCWithSlotMarker<React.PropsWithChildren<ActionListDes

// Extract text content from rendered DOM for tooltip
React.useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (truncate && containerRef.current) {
const el = containerRef.current
const textContent = el.textContent || ''
// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change
setComputedTitle(textContent)
if (setTruncatedText) {
setTruncatedText(
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr

useEffect(() => {
// ensure overlay ref gets cleared when closed, so position can reset between closing/re-opening
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (!open && overlayRef.current) {
updateOverlayRef(null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ export const InADialog = () => {
useEffect(() => {
if (outerContainerRef.current instanceof HTMLElement) {
registerPortalRoot(outerContainerRef.current, 'outerContainer')
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
setMounted(true)
}
}, [isDialogOpen])
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Autocomplete/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ const AutocompleteInput = React.forwardRef(

if (
isInputFocused &&
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
highlightRemainingText &&
autocompleteSuggestion &&
(inputValue || isMenuDirectlyActivated)
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/Autocomplete/AutocompleteMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
useEffect(() => {
// Use deferredInputValue to avoid running this effect on every keystroke
// The Input component guards against stale suggestions
// eslint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent, react-you-might-not-need-an-effect/no-pass-live-state-to-parent
if (highlightedItem?.text?.startsWith(deferredInputValue) && !selectedItemIds.includes(highlightedItem.id)) {
setAutocompleteSuggestion(highlightedItem.text)
} else {
Expand All @@ -331,15 +332,18 @@ function AutocompleteMenu<T extends AutocompleteItemProps>(props: AutocompleteMe
itemIdSortResult.length === sortedItemIds.length &&
itemIdSortResult.every((element, index) => element === sortedItemIds[index])

// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (showMenu === false && !sortResultMatchesState) {
// eslint-disable-next-line react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-derived-state
setSortedItemIds(itemIdSortResult)
}

// eslint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent
onOpenChange && onOpenChange(Boolean(showMenu))
}, [showMenu, onOpenChange, selectedItemIds, sortOnCloseFn, sortedItemIds])

useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (selectedItemIds.length) {
setSelectedItemLength(selectedItemIds.length)
}
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/AvatarStack/AvatarStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const AvatarStack = ({
observer.observe(stackContainer.current, {childList: true})

// Call on initial render, then call it again only if there's a mutation
// eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state
interactiveChildren()

return () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/Breadcrumbs/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,14 +271,18 @@ function Breadcrumbs({className, children, style, overflow = 'wrap', variant = '
useResizeObserver(handleResize, containerRef)

useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if ((overflow === 'menu' || overflow === 'menu-with-root') && childArray.length > 5 && menuItems.length === 0) {
const containerWidth = containerRef.current?.offsetWidth || 800
const result = calculateOverflow(containerWidth)

// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change, react-you-might-not-need-an-effect/no-chain-state-updates
setVisibleItems(result.visibleItems)

// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change, react-you-might-not-need-an-effect/no-chain-state-updates
setMenuItems(result.menuItems)

// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates, react-you-might-not-need-an-effect/no-derived-state
setEffectiveHideRoot(result.effectiveHideRoot)
}
}, [overflow, childArray, calculateOverflow, menuItems.length])
Expand Down
4 changes: 3 additions & 1 deletion packages/react/src/DataTable/storybook/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ export function useQuery<T>(
React.useEffect(() => {
const controller = new AbortController()

// eslint-disable-next-line react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-adjust-state-on-prop-change
setLoading(true)
// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change
setError(null)
// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change
setData(null)

savedQueryFn
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Dialog/Dialog.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export const ReproMultistepDialogWithConditionalFooter = ({width, height}: Dialo
React.useEffect(() => {
// focus the close button when the step changes
const focusTarget = dialogRef.current?.querySelector('button[aria-label="Close"]') as HTMLButtonElement
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (step === 2) {
focusTarget.focus()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ const Buttons: React.FC<React.PropsWithChildren<{buttons: DialogButtonProps[]}>>
if (hasRendered === 1) {
autoFocusRef.current?.focus()
} else {
// eslint-disable-next-line react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-derived-state
setHasRendered(hasRendered + 1)
}
}, [autoFocusRef, hasRendered])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ export function FilteredActionList({
[onListContainerRefChanged],
)
useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-pass-data-to-parent
onInputRefChanged?.(inputRef)
}, [inputRef, onInputRefChanged])

Expand Down Expand Up @@ -369,6 +370,7 @@ export function FilteredActionList({
}, [items, inputRef, scrollContainerRef, scrollBehavior])

useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (usingRovingTabindex) {
const inputAndListContainerElement = inputAndListContainerRef.current
if (!inputAndListContainerElement) return
Expand All @@ -391,7 +393,9 @@ export function FilteredActionList({
}, [items, inputRef, listContainerElement, usingRovingTabindex]) // Re-run when items change to update active indicators

useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (usingRovingTabindex && !loading) {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-adjust-state-on-prop-change
setIsInputFocused(inputRef.current && inputRef.current === document.activeElement ? true : false)
}
}, [loading, inputRef, usingRovingTabindex])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const useAnnouncements = (

liveRegion?.clear() // clear previous announcements

// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (items.length === 0 && !loading) {
announce(`${message?.title}. ${message?.description}`, {delayMs})
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,11 @@ export const WithCustomInput = () => {

React.useEffect(() => {
if (doesValueContainSpaces(value)) {
// eslint-disable-next-line react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-chain-state-updates
setValidationResult('noSpaces')
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
} else if (value) {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
setValidationResult('validName')
}
}, [value])
Expand Down Expand Up @@ -241,9 +243,11 @@ export const ValidationExample = () => {

React.useEffect(() => {
if (doesValueContainSpaces(value)) {
// eslint-disable-next-line react-hooks/set-state-in-effect
// eslint-disable-next-line react-hooks/set-state-in-effect, react-you-might-not-need-an-effect/no-chain-state-updates
setValidationResult('noSpaces')
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
} else if (value) {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
setValidationResult('validName')
}
}, [value])
Expand Down
4 changes: 4 additions & 0 deletions packages/react/src/LabelGroup/LabelGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({

React.useEffect(() => {
// If we're not truncating, we don't need to run this useEffect.
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (!visibleChildCount || isOverflowShown) {
return
}
Expand Down Expand Up @@ -255,6 +256,7 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({
// We need to keep track of this so we can focus the first hidden child when the overflow is shown inline.
React.useEffect(() => {
// If we're using an overlay, we don't need to keep track of the first hidden index.
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (overflowStyle === 'overlay') {
return
}
Expand All @@ -267,12 +269,14 @@ const LabelGroup: React.FC<React.PropsWithChildren<LabelGroupProps>> = ({
// We need to keep track of this so we can focus the first hidden child when the overflow is shown inline.
React.useEffect(() => {
// If we're using an overlay, we don't need to focus the first child that was previously hidden.
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (overflowStyle === 'overlay') {
return
}
const firstHiddenChildDOM = document.querySelector<HTMLElement>(`[data-index="${firstHiddenIndexRef.current}"]`)
const focusableChild = firstHiddenChildDOM ? getFocusableChild(firstHiddenChildDOM) : null

// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (isOverflowShown) {
// If the first hidden child is focusable, focus it.
// Otherwise, focus the collapse button.
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/Overlay/Overlay.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ export const MemexIssueOverlay = ({role, open}: Args) => {

React.useEffect(() => {
// If we just started editing, focus the newly rendered input
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (editing) inputRef.current?.focus()
}, [editing])

Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/Portal/Portal.features.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const CustomPortalRootByRegistration: React.FC<React.PropsWithChildren<Re
React.useEffect(() => {
if (outerContainerRef.current instanceof HTMLElement) {
registerPortalRoot(outerContainerRef.current)
// eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state
setMounted(true)
}
}, [])
Expand Down Expand Up @@ -92,6 +93,7 @@ export const WithPortalContext = () => {
if (customContainerRef.current instanceof HTMLElement && overrideContainerRef.current instanceof HTMLElement) {
registerPortalRoot(customContainerRef.current, 'custom-portal')
registerPortalRoot(overrideContainerRef.current, 'override-portal')
// eslint-disable-next-line react-you-might-not-need-an-effect/no-initialize-state
setMounted(true)
}
}, [])
Expand Down
2 changes: 2 additions & 0 deletions packages/react/src/SelectPanel/SelectPanel.dev.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,10 @@ export const LotsOfItems = () => {
}

useEffect(() => {
// eslint-disable-next-line react-you-might-not-need-an-effect/no-event-handler
if (open) {
timeAfterOpen.current = performance.now()
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
if (timeBeforeOpen.current) setTimeTakenToOpen(timeAfterOpen.current - timeBeforeOpen.current)
}
}, [open])
Expand Down
Loading
Loading