Skip to content

React: Fix performance issue with useTopLayer in causing re-renders of all consumers on page whenever a single one changes #3662

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -659,7 +659,7 @@ export type ComboboxProps<
} | null

onClose?(): void

outsideClickScope?: string
__demoMode?: boolean
}
>
@@ -685,6 +685,7 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
// Deprecated, but let's pluck it from the props such that it doesn't end up
// on the `Fragment`
nullable: _nullable,
outsideClickScope,
...theirProps
} = props
let defaultValue = useDefaultValue(_defaultValue)
@@ -806,7 +807,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
useOutsideClick(
outsideClickEnabled,
[data.buttonElement, data.inputElement, data.optionsElement],
() => actions.closeCombobox()
() => actions.closeCombobox(),
outsideClickScope
)

let slot = useMemo(() => {
15 changes: 11 additions & 4 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
@@ -126,6 +126,7 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
autoFocus = true,
__demoMode = false,
unmount = false,
outsideClickScope,
...theirProps
} = props

@@ -213,10 +214,15 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
})

// Close Dialog on outside click
useOutsideClick(enabled, resolveRootContainers, (event) => {
event.preventDefault()
close()
})
useOutsideClick(
enabled,
resolveRootContainers,
(event) => {
event.preventDefault()
close()
},
outsideClickScope
)

// Handle `Escape` to close
useEscape(enabled, ownerDocument?.defaultView, (event) => {
@@ -347,6 +353,7 @@ export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> =
role?: 'dialog' | 'alertdialog'
autoFocus?: boolean
transition?: boolean
outsideClickScope?: string
__demoMode?: boolean
}
>
6 changes: 4 additions & 2 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
@@ -492,7 +492,7 @@ export type ListboxProps<
form?: string
name?: string
multiple?: boolean

outsideClickScope?: string
__demoMode?: boolean
}
>
@@ -515,6 +515,7 @@ function ListboxFn<
horizontal = false,
multiple = false,
__demoMode = false,
outsideClickScope,
...theirProps
} = props

@@ -592,7 +593,8 @@ function ListboxFn<
event.preventDefault()
data.buttonElement?.focus()
}
}
},
outsideClickScope
)

let slot = useMemo(() => {
22 changes: 14 additions & 8 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
@@ -382,14 +382,15 @@ export type MenuProps<TTag extends ElementType = typeof DEFAULT_MENU_TAG> = Prop
MenuPropsWeControl,
{
__demoMode?: boolean
outsideClickScope?: string
}
>

function MenuFn<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
props: MenuProps<TTag>,
ref: Ref<HTMLElement>
) {
let { __demoMode = false, ...theirProps } = props
let { __demoMode = false, outsideClickScope, ...theirProps } = props
let reducerBag = useReducer(stateReducer, {
__demoMode,
menuState: __demoMode ? MenuStates.Open : MenuStates.Closed,
@@ -405,14 +406,19 @@ function MenuFn<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(

// Handle outside click
let outsideClickEnabled = menuState === MenuStates.Open
useOutsideClick(outsideClickEnabled, [buttonElement, itemsElement], (event, target) => {
dispatch({ type: ActionTypes.CloseMenu })
useOutsideClick(
outsideClickEnabled,
[buttonElement, itemsElement],
(event, target) => {
dispatch({ type: ActionTypes.CloseMenu })

if (!isFocusableElement(target, FocusableMode.Loose)) {
event.preventDefault()
buttonElement?.focus()
}
})
if (!isFocusableElement(target, FocusableMode.Loose)) {
event.preventDefault()
buttonElement?.focus()
}
},
outsideClickScope
)

let close = useEvent(() => {
dispatch({ type: ActionTypes.CloseMenu })
22 changes: 14 additions & 8 deletions packages/@headlessui-react/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
@@ -238,14 +238,15 @@ export type PopoverProps<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>
PopoverPropsWeControl,
{
__demoMode?: boolean
outsideClickScope?: string
}
>

function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
props: PopoverProps<TTag>,
ref: Ref<HTMLElement>
) {
let { __demoMode = false, ...theirProps } = props
let { __demoMode = false, outsideClickScope, ...theirProps } = props
let internalPopoverRef = useRef<HTMLElement | null>(null)
let popoverRef = useSyncRefs(
ref,
@@ -375,14 +376,19 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(

// Handle outside click
let outsideClickEnabled = popoverState === PopoverStates.Open
useOutsideClick(outsideClickEnabled, root.resolveContainers, (event, target) => {
dispatch({ type: ActionTypes.ClosePopover })
useOutsideClick(
outsideClickEnabled,
root.resolveContainers,
(event, target) => {
dispatch({ type: ActionTypes.ClosePopover })

if (!isFocusableElement(target, FocusableMode.Loose)) {
event.preventDefault()
button?.focus()
}
})
if (!isFocusableElement(target, FocusableMode.Loose)) {
event.preventDefault()
button?.focus()
}
},
outsideClickScope
)

let close = useEvent(
(
5 changes: 3 additions & 2 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
Original file line number Diff line number Diff line change
@@ -21,9 +21,10 @@ const MOVE_THRESHOLD_PX = 30
export function useOutsideClick(
enabled: boolean,
containers: ContainerInput | (() => ContainerInput),
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void,
topLayerScope = 'outside-click'
) {
let isTopLayer = useIsTopLayer(enabled, 'outside-click')
let isTopLayer = useIsTopLayer(enabled, topLayerScope)
let cbRef = useLatestValue(cb)

let handleOutsideClick = useCallback(