Skip to content

Commit a4953a2

Browse files
authored
Fix crash in ListboxOptions when using as={Fragment} (#3513)
This PR fixes an issue where a `Maximum update depth exceeded` error occurs if you use `as={Fragment}` in the `ListboxOptions` component. This PR also includes a refactor to make sure this exact issue cannot happen anymore in other components. Fixes: #3507
1 parent 3b047fc commit a4953a2

File tree

27 files changed

+164
-48
lines changed

27 files changed

+164
-48
lines changed

packages/@headlessui-react/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Use `React.JSX` instead of deprecated global `JSX` ([#3511](https://github.com/tailwindlabs/headlessui/pull/3511))
13+
- Fix crash in `ListboxOptions` when using `as={Fragment}` ([#3513](https://github.com/tailwindlabs/headlessui/pull/3513))
1314

1415
## [2.1.9] - 2024-10-03
1516

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import type { Props } from '../../types'
99
import {
1010
forwardRefWithAs,
1111
mergeProps,
12-
render,
13-
useMergeRefsFn,
12+
useRender,
1413
type HasDisplayName,
1514
type RefProp,
1615
} from '../../utils/render'
@@ -42,7 +41,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
4241
ref: Ref<HTMLElement>
4342
) {
4443
let providedDisabled = useDisabled()
45-
let mergeRefs = useMergeRefsFn()
4644
let { disabled = providedDisabled || false, autoFocus = false, ...theirProps } = props
4745

4846
let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
@@ -65,8 +63,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
6563
return { disabled, hover, focus, active, autofocus: autoFocus } satisfies ButtonRenderPropArg
6664
}, [disabled, hover, focus, active, autoFocus])
6765

66+
let render = useRender()
67+
6868
return render({
69-
mergeRefs,
7069
ourProps,
7170
theirProps,
7271
slot,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { attemptSubmit } from '../../utils/form'
2626
import {
2727
forwardRefWithAs,
2828
mergeProps,
29-
render,
29+
useRender,
3030
type HasDisplayName,
3131
type RefProp,
3232
} from '../../utils/render'
@@ -176,6 +176,8 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
176176
return onChange?.(defaultChecked)
177177
}, [onChange, defaultChecked])
178178

179+
let render = useRender()
180+
179181
return (
180182
<>
181183
{name != null && (

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ import {
6969
RenderFeatures,
7070
forwardRefWithAs,
7171
mergeProps,
72-
render,
73-
useMergeRefsFn,
72+
useRender,
7473
type HasDisplayName,
7574
type PropsForFeatures,
7675
type RefProp,
@@ -949,6 +948,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
949948
return theirOnChange?.(defaultValue)
950949
}, [theirOnChange, defaultValue])
951950

951+
let render = useRender()
952+
952953
return (
953954
<LabelProvider
954955
value={labelledby}
@@ -1444,6 +1445,8 @@ function InputFn<
14441445
hoverProps
14451446
)
14461447

1448+
let render = useRender()
1449+
14471450
return render({
14481451
ourProps,
14491452
theirProps,
@@ -1489,7 +1492,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
14891492
let data = useData('Combobox.Button')
14901493
let actions = useActions('Combobox.Button')
14911494
let buttonRef = useSyncRefs(ref, actions.setButtonElement)
1492-
let mergeRefs = useMergeRefsFn()
14931495

14941496
let internalId = useId()
14951497
let {
@@ -1610,8 +1612,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
16101612
pressProps
16111613
)
16121614

1615+
let render = useRender()
1616+
16131617
return render({
1614-
mergeRefs,
16151618
ourProps,
16161619
theirProps,
16171620
slot,
@@ -1813,6 +1816,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
18131816
})
18141817
}
18151818

1819+
let render = useRender()
1820+
18161821
return (
18171822
<Portal enabled={portal ? props.static || visible : false}>
18181823
<ComboboxDataContext.Provider
@@ -2037,6 +2042,8 @@ function OptionFn<
20372042
onMouseLeave: handleLeave,
20382043
}
20392044

2045+
let render = useRender()
2046+
20402047
return render({
20412048
ourProps,
20422049
theirProps,

packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { Props } from '../../types'
88
import {
99
forwardRefWithAs,
1010
mergeProps,
11-
render,
11+
useRender,
1212
type HasDisplayName,
1313
type RefProp,
1414
} from '../../utils/render'
@@ -47,6 +47,8 @@ function DataInteractiveFn<TTag extends ElementType = typeof DEFAULT_DATA_INTERA
4747
[hover, focus, active]
4848
)
4949

50+
let render = useRender()
51+
5052
return render({
5153
ourProps,
5254
theirProps,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
1515
import { useSyncRefs } from '../../hooks/use-sync-refs'
1616
import { useDisabled } from '../../internal/disabled'
1717
import type { Props } from '../../types'
18-
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
18+
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
1919

2020
// ---
2121

@@ -121,6 +121,8 @@ function DescriptionFn<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG
121121
let slot = useMemo(() => ({ ...context.slot, disabled }), [context.slot, disabled])
122122
let ourProps = { ref: descriptionRef, ...context.props, id }
123123

124+
let render = useRender()
125+
124126
return render({
125127
ourProps,
126128
theirProps,

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { match } from '../../utils/match'
4141
import {
4242
RenderFeatures,
4343
forwardRefWithAs,
44-
render,
44+
useRender,
4545
type HasDisplayName,
4646
type PropsForFeatures,
4747
type RefProp,
@@ -286,6 +286,8 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
286286
}
287287
}
288288

289+
let render = useRender()
290+
289291
return (
290292
<ResetOpenClosedProvider>
291293
<ForcePortalRoot force={true}>
@@ -450,6 +452,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
450452
let Wrapper = transition ? TransitionChild : Fragment
451453
let wrapperProps = transition ? { unmount } : {}
452454

455+
let render = useRender()
456+
453457
return (
454458
<Wrapper {...wrapperProps}>
455459
{render({
@@ -494,6 +498,8 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
494498
let Wrapper = transition ? TransitionChild : Fragment
495499
let wrapperProps = transition ? { unmount } : {}
496500

501+
let render = useRender()
502+
497503
return (
498504
<Wrapper {...wrapperProps}>
499505
{render({
@@ -541,6 +547,8 @@ function TitleFn<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
541547

542548
let ourProps = { ref: titleRef, id }
543549

550+
let render = useRender()
551+
544552
return render({
545553
ourProps,
546554
theirProps,

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

+7-6
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ import {
4141
RenderFeatures,
4242
forwardRefWithAs,
4343
mergeProps,
44-
render,
45-
useMergeRefsFn,
44+
useRender,
4645
type HasDisplayName,
4746
type PropsForFeatures,
4847
type RefProp,
@@ -233,6 +232,8 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
233232
ref: disclosureRef,
234233
}
235234

235+
let render = useRender()
236+
236237
return (
237238
<DisclosureContext.Provider value={reducerBag}>
238239
<DisclosureAPIContext.Provider value={api}>
@@ -304,7 +305,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
304305
return dispatch({ type: ActionTypes.SetButtonElement, element })
305306
})
306307
)
307-
let mergeRefs = useMergeRefsFn()
308308

309309
useEffect(() => {
310310
if (isWithinPanel) return
@@ -411,8 +411,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
411411
pressProps
412412
)
413413

414+
let render = useRender()
415+
414416
return render({
415-
mergeRefs,
416417
ourProps,
417418
theirProps,
418419
slot,
@@ -451,7 +452,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
451452
} = props
452453
let [state, dispatch] = useDisclosureContext('Disclosure.Panel')
453454
let { close } = useDisclosureAPIContext('Disclosure.Panel')
454-
let mergeRefs = useMergeRefsFn()
455455

456456
// To improve the correctness of transitions (timing related race conditions),
457457
// we track the element locally to this component, instead of relying on the
@@ -496,11 +496,12 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
496496
...transitionDataAttributes(transitionData),
497497
}
498498

499+
let render = useRender()
500+
499501
return (
500502
<ResetOpenClosedProvider>
501503
<DisclosurePanelContext.Provider value={state.panelId}>
502504
{render({
503-
mergeRefs,
504505
ourProps,
505506
theirProps,
506507
slot,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { DisabledProvider, useDisabled } from '../../internal/disabled'
66
import { FormFieldsProvider } from '../../internal/form-fields'
77
import { IdProvider } from '../../internal/id'
88
import type { Props } from '../../types'
9-
import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
9+
import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
1010
import { useDescriptions } from '../description/description'
1111
import { useLabels } from '../label/label'
1212

@@ -44,6 +44,8 @@ function FieldFn<TTag extends ElementType = typeof DEFAULT_FIELD_TAG>(
4444
'aria-disabled': disabled || undefined,
4545
}
4646

47+
let render = useRender()
48+
4749
return (
4850
<DisabledProvider value={disabled}>
4951
<LabelProvider value={labelledby}>

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useResolvedTag } from '../../hooks/use-resolved-tag'
55
import { useSyncRefs } from '../../hooks/use-sync-refs'
66
import { DisabledProvider, useDisabled } from '../../internal/disabled'
77
import type { Props } from '../../types'
8-
import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
8+
import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
99
import { useLabels } from '../label/label'
1010

1111
let DEFAULT_FIELDSET_TAG = 'fieldset' as const
@@ -50,6 +50,8 @@ function FieldsetFn<TTag extends ElementType = typeof DEFAULT_FIELDSET_TAG>(
5050
'aria-disabled': disabled || undefined,
5151
}
5252

53+
let render = useRender()
54+
5355
return (
5456
<DisabledProvider value={disabled}>
5557
<LabelProvider>

packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { history } from '../../utils/active-element-history'
2424
import { Focus, FocusResult, focusElement, focusIn } from '../../utils/focus-management'
2525
import { match } from '../../utils/match'
2626
import { microTask } from '../../utils/micro-task'
27-
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
27+
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
2828

2929
type Containers =
3030
// Lazy resolved containers
@@ -197,6 +197,8 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
197197
},
198198
}
199199

200+
let render = useRender()
201+
200202
return (
201203
<>
202204
{tabLockEnabled && (

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { Props } from '../../types'
1010
import {
1111
forwardRefWithAs,
1212
mergeProps,
13-
render,
13+
useRender,
1414
type HasDisplayName,
1515
type RefProp,
1616
} from '../../utils/render'
@@ -78,6 +78,8 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
7878
return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies InputRenderPropArg
7979
}, [disabled, invalid, hover, focus, autoFocus])
8080

81+
let render = useRender()
82+
8183
return render({
8284
ourProps,
8385
theirProps,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
1717
import { useDisabled } from '../../internal/disabled'
1818
import { useProvidedId } from '../../internal/id'
1919
import type { Props } from '../../types'
20-
import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
20+
import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
2121

2222
// ---
2323

@@ -203,6 +203,8 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
203203
}
204204
}
205205

206+
let render = useRender()
207+
206208
return render({
207209
ourProps,
208210
theirProps,

0 commit comments

Comments
 (0)