Skip to content

Commit 2d3ec80

Browse files
authored
Internal refactor: use flushSync() instead of d.nextFrame() (#3263)
* use `flushSync` instead of `d.nextFrame` This guarantees that after the `flushSync` call the DOM is updated. This means that we don't have to guess and delay by a double `requestAnimationFrame` (`nextFrame`) and _hope_ that the DOM was updated already. * inline disposables call Each function in the `disposables()` object returns a cleanup function which means we can return this directly. * inline if-statements Small one, but consistent with `<Menu />` and `<Listbox />` components. * inline `flushSync()` callbacks
1 parent 479853d commit 2d3ec80

File tree

3 files changed

+49
-73
lines changed

3 files changed

+49
-73
lines changed

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import React, {
2121
type MouseEvent as ReactMouseEvent,
2222
type Ref,
2323
} from 'react'
24+
import { flushSync } from 'react-dom'
2425
import { useActivePress } from '../../hooks/use-active-press'
2526
import { useByComparator, type ByComparator } from '../../hooks/use-by-comparator'
2627
import { useControllable } from '../../hooks/use-controllable'
@@ -1189,12 +1190,8 @@ function InputFn<
11891190
return match(data.comboboxState, {
11901191
[ComboboxState.Open]: () => actions.goToOption(Focus.Previous),
11911192
[ComboboxState.Closed]: () => {
1192-
actions.openCombobox()
1193-
d.nextFrame(() => {
1194-
if (!data.value) {
1195-
actions.goToOption(Focus.Last)
1196-
}
1197-
})
1193+
flushSync(() => actions.openCombobox())
1194+
if (!data.value) actions.goToOption(Focus.Last)
11981195
},
11991196
})
12001197

@@ -1320,14 +1317,12 @@ function InputFn<
13201317
if (!data.immediate) return
13211318
if (data.comboboxState === ComboboxState.Open) return
13221319

1323-
actions.openCombobox()
1320+
flushSync(() => actions.openCombobox())
13241321

13251322
// We need to make sure that tabbing through a form doesn't result in incorrectly setting the
13261323
// value of the combobox. We will set the activation trigger to `Focus`, and we will ignore
13271324
// selecting the active option when the user tabs away.
1328-
d.nextFrame(() => {
1329-
actions.setActivationTrigger(ActivationTrigger.Focus)
1330-
})
1325+
actions.setActivationTrigger(ActivationTrigger.Focus)
13311326
})
13321327

13331328
let labelledBy = useLabelledBy()
@@ -1439,7 +1434,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
14391434
autoFocus = false,
14401435
...theirProps
14411436
} = props
1442-
let d = useDisposables()
14431437

14441438
let refocusInput = useRefocusableInput(data.inputRef)
14451439

@@ -1452,46 +1446,40 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
14521446
event.preventDefault()
14531447
event.stopPropagation()
14541448
if (data.comboboxState === ComboboxState.Closed) {
1455-
actions.openCombobox()
1449+
flushSync(() => actions.openCombobox())
14561450
}
1457-
1458-
return d.nextFrame(() => refocusInput())
1451+
refocusInput()
1452+
return
14591453

14601454
case Keys.ArrowDown:
14611455
event.preventDefault()
14621456
event.stopPropagation()
14631457
if (data.comboboxState === ComboboxState.Closed) {
1464-
actions.openCombobox()
1465-
d.nextFrame(() => {
1466-
if (!data.value) {
1467-
actions.goToOption(Focus.First)
1468-
}
1469-
})
1458+
flushSync(() => actions.openCombobox())
1459+
if (!data.value) actions.goToOption(Focus.First)
14701460
}
1471-
1472-
return d.nextFrame(() => refocusInput())
1461+
refocusInput()
1462+
return
14731463

14741464
case Keys.ArrowUp:
14751465
event.preventDefault()
14761466
event.stopPropagation()
14771467
if (data.comboboxState === ComboboxState.Closed) {
1478-
actions.openCombobox()
1479-
d.nextFrame(() => {
1480-
if (!data.value) {
1481-
actions.goToOption(Focus.Last)
1482-
}
1483-
})
1468+
flushSync(() => actions.openCombobox())
1469+
if (!data.value) actions.goToOption(Focus.Last)
14841470
}
1485-
return d.nextFrame(() => refocusInput())
1471+
refocusInput()
1472+
return
14861473

14871474
case Keys.Escape:
14881475
if (data.comboboxState !== ComboboxState.Open) return
14891476
event.preventDefault()
14901477
if (data.optionsRef.current && !data.optionsPropsRef.current.static) {
14911478
event.stopPropagation()
14921479
}
1493-
actions.closeCombobox()
1494-
return d.nextFrame(() => refocusInput())
1480+
flushSync(() => actions.closeCombobox())
1481+
refocusInput()
1482+
return
14951483

14961484
default:
14971485
return

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React, {
2020
type MouseEvent as ReactMouseEvent,
2121
type Ref,
2222
} from 'react'
23+
import { flushSync } from 'react-dom'
2324
import { useActivePress } from '../../hooks/use-active-press'
2425
import { useByComparator, type ByComparator } from '../../hooks/use-by-comparator'
2526
import { useComputed } from '../../hooks/use-computed'
@@ -755,8 +756,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
755756
let buttonRef = useSyncRefs(data.buttonRef, ref, useFloatingReference())
756757
let getFloatingReferenceProps = useFloatingReferenceProps()
757758

758-
let d = useDisposables()
759-
760759
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
761760
switch (event.key) {
762761
// Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/#keyboard-interaction-13
@@ -768,18 +767,14 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
768767
case Keys.Space:
769768
case Keys.ArrowDown:
770769
event.preventDefault()
771-
actions.openListbox()
772-
d.nextFrame(() => {
773-
if (!data.value) actions.goToOption(Focus.First)
774-
})
770+
flushSync(() => actions.openListbox())
771+
if (!data.value) actions.goToOption(Focus.First)
775772
break
776773

777774
case Keys.ArrowUp:
778775
event.preventDefault()
779-
actions.openListbox()
780-
d.nextFrame(() => {
781-
if (!data.value) actions.goToOption(Focus.Last)
782-
})
776+
flushSync(() => actions.openListbox())
777+
if (!data.value) actions.goToOption(Focus.Last)
783778
break
784779
}
785780
})
@@ -798,8 +793,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
798793
let handleClick = useEvent((event: ReactMouseEvent) => {
799794
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
800795
if (data.listboxState === ListboxStates.Open) {
801-
actions.closeListbox()
802-
d.nextFrame(() => data.buttonRef.current?.focus({ preventScroll: true }))
796+
flushSync(() => actions.closeListbox())
797+
data.buttonRef.current?.focus({ preventScroll: true })
803798
} else {
804799
event.preventDefault()
805800
actions.openListbox()
@@ -995,7 +990,6 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
995990
let getFloatingPanelProps = useFloatingPanelProps()
996991
let optionsRef = useSyncRefs(data.optionsRef, ref, anchor ? floatingRef : null)
997992

998-
let d = useDisposables()
999993
let searchDisposables = useDisposables()
1000994

1001995
useEffect(() => {
@@ -1030,8 +1024,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
10301024
actions.onChange(dataRef.current.value)
10311025
}
10321026
if (data.mode === ValueMode.Single) {
1033-
actions.closeListbox()
1034-
disposables().nextFrame(() => data.buttonRef.current?.focus({ preventScroll: true }))
1027+
flushSync(() => actions.closeListbox())
1028+
data.buttonRef.current?.focus({ preventScroll: true })
10351029
}
10361030
break
10371031

@@ -1060,8 +1054,9 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
10601054
case Keys.Escape:
10611055
event.preventDefault()
10621056
event.stopPropagation()
1063-
actions.closeListbox()
1064-
return d.nextFrame(() => data.buttonRef.current?.focus({ preventScroll: true }))
1057+
flushSync(() => actions.closeListbox())
1058+
data.buttonRef.current?.focus({ preventScroll: true })
1059+
return
10651060

10661061
case Keys.Tab:
10671062
event.preventDefault()
@@ -1205,11 +1200,9 @@ function OptionFn<
12051200
if (data.listboxState !== ListboxStates.Open) return
12061201
if (!active) return
12071202
if (data.activationTrigger === ActivationTrigger.Pointer) return
1208-
let d = disposables()
1209-
d.requestAnimationFrame(() => {
1203+
return disposables().requestAnimationFrame(() => {
12101204
internalOptionRef.current?.scrollIntoView?.({ block: 'nearest' })
12111205
})
1212-
return d.dispose
12131206
}, [
12141207
internalOptionRef,
12151208
active,
@@ -1228,8 +1221,8 @@ function OptionFn<
12281221
if (disabled) return event.preventDefault()
12291222
actions.onChange(value)
12301223
if (data.mode === ValueMode.Single) {
1231-
actions.closeListbox()
1232-
disposables().nextFrame(() => data.buttonRef.current?.focus({ preventScroll: true }))
1224+
flushSync(() => actions.closeListbox())
1225+
data.buttonRef.current?.focus({ preventScroll: true })
12331226
}
12341227
})
12351228

packages/@headlessui-react/src/components/menu/menu.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React, {
2020
type MouseEvent as ReactMouseEvent,
2121
type Ref,
2222
} from 'react'
23+
import { flushSync } from 'react-dom'
2324
import { useActivePress } from '../../hooks/use-active-press'
2425
import { useDidElementMove } from '../../hooks/use-did-element-move'
2526
import { useDisposables } from '../../hooks/use-disposables'
@@ -469,8 +470,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
469470
let getFloatingReferenceProps = useFloatingReferenceProps()
470471
let buttonRef = useSyncRefs(state.buttonRef, ref, useFloatingReference())
471472

472-
let d = useDisposables()
473-
474473
let handleKeyDown = useEvent((event: ReactKeyboardEvent<HTMLButtonElement>) => {
475474
switch (event.key) {
476475
// Ref: https://www.w3.org/WAI/ARIA/apg/patterns/menubutton/#keyboard-interaction-13
@@ -480,15 +479,15 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
480479
case Keys.ArrowDown:
481480
event.preventDefault()
482481
event.stopPropagation()
483-
dispatch({ type: ActionTypes.OpenMenu })
484-
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.First }))
482+
flushSync(() => dispatch({ type: ActionTypes.OpenMenu }))
483+
dispatch({ type: ActionTypes.GoToItem, focus: Focus.First })
485484
break
486485

487486
case Keys.ArrowUp:
488487
event.preventDefault()
489488
event.stopPropagation()
490-
dispatch({ type: ActionTypes.OpenMenu })
491-
d.nextFrame(() => dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last }))
489+
flushSync(() => dispatch({ type: ActionTypes.OpenMenu }))
490+
dispatch({ type: ActionTypes.GoToItem, focus: Focus.Last })
492491
break
493492
}
494493
})
@@ -508,8 +507,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
508507
if (isDisabledReactIssue7711(event.currentTarget)) return event.preventDefault()
509508
if (disabled) return
510509
if (state.menuState === MenuStates.Open) {
511-
dispatch({ type: ActionTypes.CloseMenu })
512-
d.nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
510+
flushSync(() => dispatch({ type: ActionTypes.CloseMenu }))
511+
state.buttonRef.current?.focus({ preventScroll: true })
513512
} else {
514513
event.preventDefault()
515514
dispatch({ type: ActionTypes.OpenMenu })
@@ -722,20 +721,18 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
722721
case Keys.Escape:
723722
event.preventDefault()
724723
event.stopPropagation()
725-
dispatch({ type: ActionTypes.CloseMenu })
726-
disposables().nextFrame(() => state.buttonRef.current?.focus({ preventScroll: true }))
724+
flushSync(() => dispatch({ type: ActionTypes.CloseMenu }))
725+
state.buttonRef.current?.focus({ preventScroll: true })
727726
break
728727

729728
case Keys.Tab:
730729
event.preventDefault()
731730
event.stopPropagation()
732-
dispatch({ type: ActionTypes.CloseMenu })
733-
disposables().microTask(() => {
734-
focusFrom(
735-
state.buttonRef.current!,
736-
event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next
737-
)
738-
})
731+
flushSync(() => dispatch({ type: ActionTypes.CloseMenu }))
732+
focusFrom(
733+
state.buttonRef.current!,
734+
event.shiftKey ? FocusManagementFocus.Previous : FocusManagementFocus.Next
735+
)
739736
break
740737

741738
default:
@@ -837,11 +834,9 @@ function ItemFn<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
837834
if (state.menuState !== MenuStates.Open) return
838835
if (!active) return
839836
if (state.activationTrigger === ActivationTrigger.Pointer) return
840-
let d = disposables()
841-
d.requestAnimationFrame(() => {
837+
return disposables().requestAnimationFrame(() => {
842838
internalItemRef.current?.scrollIntoView?.({ block: 'nearest' })
843839
})
844-
return d.dispose
845840
}, [
846841
state.__demoMode,
847842
internalItemRef,

0 commit comments

Comments
 (0)