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
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
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ export type ComboboxProps<
} | null

onClose?(): void

outsideClickScope?: string
__demoMode?: boolean
}
>
Expand All @@ -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)
Expand Down Expand Up @@ -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(() => {
Expand Down
15 changes: 11 additions & 4 deletions packages/@headlessui-react/src/components/dialog/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
autoFocus = true,
__demoMode = false,
unmount = false,
outsideClickScope,
...theirProps
} = props

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
}
>
Expand Down
6 changes: 4 additions & 2 deletions packages/@headlessui-react/src/components/listbox/listbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ export type ListboxProps<
form?: string
name?: string
multiple?: boolean

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

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

let slot = useMemo(() => {
Expand Down
22 changes: 14 additions & 8 deletions packages/@headlessui-react/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 })
Expand Down
22 changes: 14 additions & 8 deletions packages/@headlessui-react/src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
(
Expand Down
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
Expand Up @@ -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(
Expand Down