Repository: tailwindlabs/headlessui
Pull request #3513 (2024-10-08 17:19:00)

Fix crash in `ListboxOptions` when using `as={Fragment}`
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


vercel[bot]: [vc]: #uRFfN9sYZPBuar8+dKi+WoJtDmAQw4HWH2NRqFwXmXU=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0LzVnM0xNaFFyUUtiMVBneDNrR01RcXBXcWRTMUciLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZml4LWlzc3VlLTM1MDctdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXJlYWN0LWdpdC1maXgtaXNzdWUtMzUwNy10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/5g3LMhQrQKb1Pgx3kGMQqpWqdS1G)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-react-git-fix-issue-3507-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-fix-issue-3507-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Oct 8, 2024 5:19pm |



Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 4ff64b2..e9a09c9 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Fixed
 
 - Use `React.JSX` instead of deprecated global `JSX` ([#3511](https://github.com/tailwindlabs/headlessui/pull/3511))
+- Fix crash in `ListboxOptions` when using `as={Fragment}` ([#3513](https://github.com/tailwindlabs/headlessui/pull/3513))
 
 ## [2.1.9] - 2024-10-03
 
diff --git a/packages/@headlessui-react/src/components/button/button.tsx b/packages/@headlessui-react/src/components/button/button.tsx
index 174b6a5..2ead53d 100644
--- a/packages/@headlessui-react/src/components/button/button.tsx
+++ b/packages/@headlessui-react/src/components/button/button.tsx
@@ -9,8 +9,7 @@ import type { Props } from '../../types'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -42,7 +41,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   ref: Ref<HTMLElement>
 ) {
   let providedDisabled = useDisabled()
-  let mergeRefs = useMergeRefsFn()
   let { disabled = providedDisabled || false, autoFocus = false, ...theirProps } = props
 
   let { isFocusVisible: focus, focusProps } = useFocusRing({ autoFocus })
@@ -65,8 +63,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     return { disabled, hover, focus, active, autofocus: autoFocus } satisfies ButtonRenderPropArg
   }, [disabled, hover, focus, active, autoFocus])
 
+  let render = useRender()
+
   return render({
-    mergeRefs,
     ourProps,
     theirProps,
     slot,
diff --git a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
index f68ed34..c964ff5 100644
--- a/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
+++ b/packages/@headlessui-react/src/components/checkbox/checkbox.tsx
@@ -26,7 +26,7 @@ import { attemptSubmit } from '../../utils/form'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -176,6 +176,8 @@ function CheckboxFn<TTag extends ElementType = typeof DEFAULT_CHECKBOX_TAG, TTyp
     return onChange?.(defaultChecked)
   }, [onChange, defaultChecked])
 
+  let render = useRender()
+
   return (
     <>
       {name != null && (
diff --git a/packages/@headlessui-react/src/components/combobox/combobox.tsx b/packages/@headlessui-react/src/components/combobox/combobox.tsx
index b6ab9e1..8c395f9 100644
--- a/packages/@headlessui-react/src/components/combobox/combobox.tsx
+++ b/packages/@headlessui-react/src/components/combobox/combobox.tsx
@@ -69,8 +69,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -949,6 +948,8 @@ function ComboboxFn<TValue, TTag extends ElementType = typeof DEFAULT_COMBOBOX_T
     return theirOnChange?.(defaultValue)
   }, [theirOnChange, defaultValue])
 
+  let render = useRender()
+
   return (
     <LabelProvider
       value={labelledby}
@@ -1444,6 +1445,8 @@ function InputFn<
     hoverProps
   )
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -1489,7 +1492,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   let data = useData('Combobox.Button')
   let actions = useActions('Combobox.Button')
   let buttonRef = useSyncRefs(ref, actions.setButtonElement)
-  let mergeRefs = useMergeRefsFn()
 
   let internalId = useId()
   let {
@@ -1610,8 +1612,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     pressProps
   )
 
+  let render = useRender()
+
   return render({
-    mergeRefs,
     ourProps,
     theirProps,
     slot,
@@ -1813,6 +1816,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     })
   }
 
+  let render = useRender()
+
   return (
     <Portal enabled={portal ? props.static || visible : false}>
       <ComboboxDataContext.Provider
@@ -2037,6 +2042,8 @@ function OptionFn<
     onMouseLeave: handleLeave,
   }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx b/packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx
index b85eb11..acc0de4 100644
--- a/packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx
+++ b/packages/@headlessui-react/src/components/data-interactive/data-interactive.tsx
@@ -8,7 +8,7 @@ import type { Props } from '../../types'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -47,6 +47,8 @@ function DataInteractiveFn<TTag extends ElementType = typeof DEFAULT_DATA_INTERA
     [hover, focus, active]
   )
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/description/description.tsx b/packages/@headlessui-react/src/components/description/description.tsx
index d9a243d..eac736f 100644
--- a/packages/@headlessui-react/src/components/description/description.tsx
+++ b/packages/@headlessui-react/src/components/description/description.tsx
@@ -15,7 +15,7 @@ import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect'
 import { useSyncRefs } from '../../hooks/use-sync-refs'
 import { useDisabled } from '../../internal/disabled'
 import type { Props } from '../../types'
-import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
 
 // ---
 
@@ -121,6 +121,8 @@ function DescriptionFn<TTag extends ElementType = typeof DEFAULT_DESCRIPTION_TAG
   let slot = useMemo(() => ({ ...context.slot, disabled }), [context.slot, disabled])
   let ourProps = { ref: descriptionRef, ...context.props, id }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx
index f4e965b..590660e 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx
@@ -41,7 +41,7 @@ import { match } from '../../utils/match'
 import {
   RenderFeatures,
   forwardRefWithAs,
-  render,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -286,6 +286,8 @@ let InternalDialog = forwardRefWithAs(function InternalDialog<
     }
   }
 
+  let render = useRender()
+
   return (
     <ResetOpenClosedProvider>
       <ForcePortalRoot force={true}>
@@ -450,6 +452,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
   let Wrapper = transition ? TransitionChild : Fragment
   let wrapperProps = transition ? { unmount } : {}
 
+  let render = useRender()
+
   return (
     <Wrapper {...wrapperProps}>
       {render({
@@ -494,6 +498,8 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
   let Wrapper = transition ? TransitionChild : Fragment
   let wrapperProps = transition ? { unmount } : {}
 
+  let render = useRender()
+
   return (
     <Wrapper {...wrapperProps}>
       {render({
@@ -541,6 +547,8 @@ function TitleFn<TTag extends ElementType = typeof DEFAULT_TITLE_TAG>(
 
   let ourProps = { ref: titleRef, id }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index 1ae9684..03a6e92 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -41,8 +41,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -233,6 +232,8 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
     ref: disclosureRef,
   }
 
+  let render = useRender()
+
   return (
     <DisclosureContext.Provider value={reducerBag}>
       <DisclosureAPIContext.Provider value={api}>
@@ -304,7 +305,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
       return dispatch({ type: ActionTypes.SetButtonElement, element })
     })
   )
-  let mergeRefs = useMergeRefsFn()
 
   useEffect(() => {
     if (isWithinPanel) return
@@ -411,8 +411,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
         pressProps
       )
 
+  let render = useRender()
+
   return render({
-    mergeRefs,
     ourProps,
     theirProps,
     slot,
@@ -451,7 +452,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
   } = props
   let [state, dispatch] = useDisclosureContext('Disclosure.Panel')
   let { close } = useDisclosureAPIContext('Disclosure.Panel')
-  let mergeRefs = useMergeRefsFn()
 
   // To improve the correctness of transitions (timing related race conditions),
   // 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>(
     ...transitionDataAttributes(transitionData),
   }
 
+  let render = useRender()
+
   return (
     <ResetOpenClosedProvider>
       <DisclosurePanelContext.Provider value={state.panelId}>
         {render({
-          mergeRefs,
           ourProps,
           theirProps,
           slot,
diff --git a/packages/@headlessui-react/src/components/field/field.tsx b/packages/@headlessui-react/src/components/field/field.tsx
index 27d9ae1..c521341 100644
--- a/packages/@headlessui-react/src/components/field/field.tsx
+++ b/packages/@headlessui-react/src/components/field/field.tsx
@@ -6,7 +6,7 @@ import { DisabledProvider, useDisabled } from '../../internal/disabled'
 import { FormFieldsProvider } from '../../internal/form-fields'
 import { IdProvider } from '../../internal/id'
 import type { Props } from '../../types'
-import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
 import { useDescriptions } from '../description/description'
 import { useLabels } from '../label/label'
 
@@ -44,6 +44,8 @@ function FieldFn<TTag extends ElementType = typeof DEFAULT_FIELD_TAG>(
     'aria-disabled': disabled || undefined,
   }
 
+  let render = useRender()
+
   return (
     <DisabledProvider value={disabled}>
       <LabelProvider value={labelledby}>
diff --git a/packages/@headlessui-react/src/components/fieldset/fieldset.tsx b/packages/@headlessui-react/src/components/fieldset/fieldset.tsx
index ae8a1ea..c30361c 100644
--- a/packages/@headlessui-react/src/components/fieldset/fieldset.tsx
+++ b/packages/@headlessui-react/src/components/fieldset/fieldset.tsx
@@ -5,7 +5,7 @@ import { useResolvedTag } from '../../hooks/use-resolved-tag'
 import { useSyncRefs } from '../../hooks/use-sync-refs'
 import { DisabledProvider, useDisabled } from '../../internal/disabled'
 import type { Props } from '../../types'
-import { forwardRefWithAs, render, type HasDisplayName } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName } from '../../utils/render'
 import { useLabels } from '../label/label'
 
 let DEFAULT_FIELDSET_TAG = 'fieldset' as const
@@ -50,6 +50,8 @@ function FieldsetFn<TTag extends ElementType = typeof DEFAULT_FIELDSET_TAG>(
           'aria-disabled': disabled || undefined,
         }
 
+  let render = useRender()
+
   return (
     <DisabledProvider value={disabled}>
       <LabelProvider>
diff --git a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
index 6ee1a53..482f2dd 100644
--- a/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
+++ b/packages/@headlessui-react/src/components/focus-trap/focus-trap.tsx
@@ -24,7 +24,7 @@ import { history } from '../../utils/active-element-history'
 import { Focus, FocusResult, focusElement, focusIn } from '../../utils/focus-management'
 import { match } from '../../utils/match'
 import { microTask } from '../../utils/micro-task'
-import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
 
 type Containers =
   // Lazy resolved containers
@@ -197,6 +197,8 @@ function FocusTrapFn<TTag extends ElementType = typeof DEFAULT_FOCUS_TRAP_TAG>(
     },
   }
 
+  let render = useRender()
+
   return (
     <>
       {tabLockEnabled && (
diff --git a/packages/@headlessui-react/src/components/input/input.tsx b/packages/@headlessui-react/src/components/input/input.tsx
index 212465e..e4665dc 100644
--- a/packages/@headlessui-react/src/components/input/input.tsx
+++ b/packages/@headlessui-react/src/components/input/input.tsx
@@ -10,7 +10,7 @@ import type { Props } from '../../types'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -78,6 +78,8 @@ function InputFn<TTag extends ElementType = typeof DEFAULT_INPUT_TAG>(
     return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies InputRenderPropArg
   }, [disabled, invalid, hover, focus, autoFocus])
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/label/label.tsx b/packages/@headlessui-react/src/components/label/label.tsx
index 73c7c96..edde216 100644
--- a/packages/@headlessui-react/src/components/label/label.tsx
+++ b/packages/@headlessui-react/src/components/label/label.tsx
@@ -17,7 +17,7 @@ import { useSyncRefs } from '../../hooks/use-sync-refs'
 import { useDisabled } from '../../internal/disabled'
 import { useProvidedId } from '../../internal/id'
 import type { Props } from '../../types'
-import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
 
 // ---
 
@@ -203,6 +203,8 @@ function LabelFn<TTag extends ElementType = typeof DEFAULT_LABEL_TAG>(
     }
   }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/listbox/listbox.tsx b/packages/@headlessui-react/src/components/listbox/listbox.tsx
index 7fc4e4a..fdde28d 100644
--- a/packages/@headlessui-react/src/components/listbox/listbox.tsx
+++ b/packages/@headlessui-react/src/components/listbox/listbox.tsx
@@ -74,8 +74,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -698,6 +697,8 @@ function ListboxFn<
     return theirOnChange?.(defaultValue)
   }, [theirOnChange, defaultValue])
 
+  let render = useRender()
+
   return (
     <LabelProvider
       value={labelledby}
@@ -786,7 +787,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     autoFocus = false,
     ...theirProps
   } = props
-  let mergeRefs = useMergeRefsFn()
   let buttonRef = useSyncRefs(ref, useFloatingReference(), actions.setButtonElement)
   let getFloatingReferenceProps = useFloatingReferenceProps()
 
@@ -881,8 +881,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     pressProps
   )
 
+  let render = useRender()
+
   return render({
-    mergeRefs,
     ourProps,
     theirProps,
     slot,
@@ -1159,6 +1160,8 @@ function OptionsFn<TTag extends ElementType = typeof DEFAULT_OPTIONS_TAG>(
     ...transitionDataAttributes(transitionData),
   })
 
+  let render = useRender()
+
   return (
     <Portal enabled={portal ? props.static || visible : false}>
       <ListboxDataContext.Provider
@@ -1336,6 +1339,8 @@ function OptionFn<
       }
     : {}
 
+  let render = useRender()
+
   if (!selected && usedInSelectedOption) {
     return null
   }
@@ -1383,6 +1388,8 @@ function SelectedFn<TTag extends ElementType = typeof DEFAULT_SELECTED_OPTION_TA
     data.value === null ||
     (data.mode === ValueMode.Multi && Array.isArray(data.value) && data.value.length === 0)
 
+  let render = useRender()
+
   return (
     <SelectedOptionContext.Provider value={true}>
       {render({
diff --git a/packages/@headlessui-react/src/components/menu/menu.tsx b/packages/@headlessui-react/src/components/menu/menu.tsx
index 251cc02..f93afd2 100644
--- a/packages/@headlessui-react/src/components/menu/menu.tsx
+++ b/packages/@headlessui-react/src/components/menu/menu.tsx
@@ -67,8 +67,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -426,6 +425,8 @@ function MenuFn<TTag extends ElementType = typeof DEFAULT_MENU_TAG>(
 
   let ourProps = { ref: menuRef }
 
+  let render = useRender()
+
   return (
     <FloatingProvider>
       <MenuContext.Provider value={reducerBag}>
@@ -484,7 +485,6 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
   } = props
   let [state, dispatch] = useMenuContext('Menu.Button')
   let getFloatingReferenceProps = useFloatingReferenceProps()
-  let mergeRefs = useMergeRefsFn()
   let buttonRef = useSyncRefs(
     ref,
     useFloatingReference(),
@@ -571,8 +571,9 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     pressProps
   )
 
+  let render = useRender()
+
   return render({
-    mergeRefs,
     ourProps,
     theirProps,
     slot,
@@ -820,6 +821,8 @@ function ItemsFn<TTag extends ElementType = typeof DEFAULT_ITEMS_TAG>(
     ...transitionDataAttributes(transitionData),
   })
 
+  let render = useRender()
+
   return (
     <Portal enabled={portal ? props.static || visible : false}>
       {render({
@@ -982,6 +985,8 @@ function ItemFn<TTag extends ElementType = typeof DEFAULT_ITEM_TAG>(
     onMouseLeave: handleLeave,
   }
 
+  let render = useRender()
+
   return (
     <LabelProvider>
       <DescriptionProvider>
@@ -1018,6 +1023,8 @@ function SectionFn<TTag extends ElementType = typeof DEFAULT_SECTION_TAG>(
   let theirProps = props
   let ourProps = { ref, 'aria-labelledby': labelledby, role: 'group' }
 
+  let render = useRender()
+
   return (
     <LabelProvider>
       {render({
@@ -1055,6 +1062,8 @@ function HeadingFn<TTag extends ElementType = typeof DEFAULT_HEADING_TAG>(
 
   let ourProps = { id, ref, role: 'presentation', ...context.props }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -1083,6 +1092,8 @@ function SeparatorFn<TTag extends ElementType = typeof DEFAULT_SEPARATOR_TAG>(
   let theirProps = props
   let ourProps = { ref, role: 'separator' }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx
index 386ec35..3d772c2 100644
--- a/packages/@headlessui-react/src/components/popover/popover.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.tsx
@@ -74,8 +74,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
-  useMergeRefsFn,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -419,6 +418,8 @@ function PopoverFn<TTag extends ElementType = typeof DEFAULT_POPOVER_TAG>(
 
   let ourProps = { ref: popoverRef }
 
+  let render = useRender()
+
   return (
     <MainTreeProvider node={mainTreeNode}>
       <FloatingProvider>
@@ -707,6 +708,8 @@ function ButtonFn<TTag extends ElementType = typeof DEFAULT_BUTTON_TAG>(
     }
   })
 
+  let render = useRender()
+
   return (
     <>
       {render({
@@ -799,6 +802,8 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
     ...transitionDataAttributes(transitionData),
   }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -884,7 +889,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
     setLocalPanelElement
   )
   let ownerDocument = useOwnerDocument(internalPanelRef)
-  let mergeRefs = useMergeRefsFn()
 
   useIsoMorphicEffect(() => {
     dispatch({ type: ActionTypes.SetPanelId, panelId: id })
@@ -1070,6 +1074,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
     }
   })
 
+  let render = useRender()
+
   return (
     <ResetOpenClosedProvider>
       <PopoverPanelContext.Provider value={id}>
@@ -1087,7 +1093,6 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
               />
             )}
             {render({
-              mergeRefs,
               ourProps,
               theirProps,
               slot,
@@ -1188,6 +1193,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
   let theirProps = props
   let ourProps = { ref: groupRef }
 
+  let render = useRender()
+
   return (
     <MainTreeProvider>
       <PopoverGroupContext.Provider value={contextBag}>
diff --git a/packages/@headlessui-react/src/components/portal/portal.tsx b/packages/@headlessui-react/src/components/portal/portal.tsx
index efc0cba..4c05ce0 100644
--- a/packages/@headlessui-react/src/components/portal/portal.tsx
+++ b/packages/@headlessui-react/src/components/portal/portal.tsx
@@ -23,7 +23,7 @@ import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
 import { usePortalRoot } from '../../internal/portal-force-root'
 import type { Props } from '../../types'
 import { env } from '../../utils/env'
-import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../../utils/render'
 
 function usePortalTarget(ref: MutableRefObject<HTMLElement | null>): HTMLElement | null {
   let forceInRoot = usePortalRoot()
@@ -129,6 +129,7 @@ let InternalPortalFn = forwardRefWithAs(function InternalPortalFn<
     }
   })
 
+  let render = useRender()
   if (!ready) return null
 
   let ourProps = { ref: portalRef }
@@ -154,6 +155,9 @@ function PortalFn<TTag extends ElementType = typeof DEFAULT_PORTAL_TAG>(
   let portalRef = useSyncRefs(ref)
 
   let { enabled = true, ...theirProps } = props
+
+  let render = useRender()
+
   return enabled ? (
     <InternalPortalFn {...theirProps} ref={portalRef} />
   ) : (
@@ -193,6 +197,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
 
   let ourProps = { ref: groupRef }
 
+  let render = useRender()
+
   return (
     <PortalGroupContext.Provider value={target}>
       {render({
diff --git a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
index bfeaee8..a43d334 100644
--- a/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
+++ b/packages/@headlessui-react/src/components/radio-group/radio-group.tsx
@@ -35,7 +35,7 @@ import { getOwnerDocument } from '../../utils/owner'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -309,6 +309,8 @@ function RadioGroupFn<TTag extends ElementType = typeof DEFAULT_RADIO_GROUP_TAG,
     return triggerChange(defaultValue)
   }, [triggerChange, defaultValue])
 
+  let render = useRender()
+
   return (
     <DescriptionProvider name="RadioGroup.Description">
       <LabelProvider name="RadioGroup.Label">
@@ -444,6 +446,8 @@ function OptionFn<
     } satisfies OptionRenderPropArg
   }, [checked, disabled, hover, focus, autoFocus])
 
+  let render = useRender()
+
   return (
     <DescriptionProvider name="RadioGroup.Description">
       <LabelProvider name="RadioGroup.Label">
@@ -557,6 +561,8 @@ function RadioFn<
     return { checked, disabled, hover, focus, autofocus: autoFocus } satisfies RadioRenderPropArg
   }, [checked, disabled, hover, focus, autoFocus])
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/select/select.tsx b/packages/@headlessui-react/src/components/select/select.tsx
index afcc900..82d0653 100644
--- a/packages/@headlessui-react/src/components/select/select.tsx
+++ b/packages/@headlessui-react/src/components/select/select.tsx
@@ -11,7 +11,7 @@ import type { Props } from '../../types'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -89,6 +89,8 @@ function SelectFn<TTag extends ElementType = typeof DEFAULT_SELECT_TAG>(
     } satisfies SelectRenderPropArg
   }, [disabled, invalid, hover, focus, active, autoFocus])
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/switch/switch.tsx b/packages/@headlessui-react/src/components/switch/switch.tsx
index 989fe81..f3c9df6 100644
--- a/packages/@headlessui-react/src/components/switch/switch.tsx
+++ b/packages/@headlessui-react/src/components/switch/switch.tsx
@@ -32,7 +32,7 @@ import { attemptSubmit } from '../../utils/form'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -74,6 +74,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_GROUP_TAG>(
   let ourProps = {}
   let theirProps = props
 
+  let render = useRender()
+
   return (
     <DescriptionProvider name="Switch.Description" value={describedby}>
       <LabelProvider
@@ -244,6 +246,8 @@ function SwitchFn<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG>(
     return onChange?.(defaultChecked)
   }, [onChange, defaultChecked])
 
+  let render = useRender()
+
   return (
     <>
       {name != null && (
diff --git a/packages/@headlessui-react/src/components/tabs/tabs.tsx b/packages/@headlessui-react/src/components/tabs/tabs.tsx
index 52c9252..651c263 100644
--- a/packages/@headlessui-react/src/components/tabs/tabs.tsx
+++ b/packages/@headlessui-react/src/components/tabs/tabs.tsx
@@ -33,7 +33,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -320,6 +320,8 @@ function GroupFn<TTag extends ElementType = typeof DEFAULT_TABS_TAG>(
 
   let ourProps = { ref: tabsRef }
 
+  let render = useRender()
+
   return (
     <StableCollection>
       <TabsActionsContext.Provider value={tabsActions}>
@@ -384,6 +386,8 @@ function ListFn<TTag extends ElementType = typeof DEFAULT_LIST_TAG>(
     'aria-orientation': orientation,
   }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -556,6 +560,8 @@ function TabFn<TTag extends ElementType = typeof DEFAULT_TAB_TAG>(
     pressProps
   )
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -589,6 +595,8 @@ function PanelsFn<TTag extends ElementType = typeof DEFAULT_PANELS_TAG>(
   let theirProps = props
   let ourProps = { ref: panelsRef }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -650,6 +658,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
     focusProps
   )
 
+  let render = useRender()
+
   if (!selected && (theirProps.unmount ?? true) && !(theirProps.static ?? false)) {
     return <Hidden aria-hidden="true" {...ourProps} />
   }
diff --git a/packages/@headlessui-react/src/components/textarea/textarea.tsx b/packages/@headlessui-react/src/components/textarea/textarea.tsx
index b32cfcd..77dd671 100644
--- a/packages/@headlessui-react/src/components/textarea/textarea.tsx
+++ b/packages/@headlessui-react/src/components/textarea/textarea.tsx
@@ -10,7 +10,7 @@ import type { Props } from '../../types'
 import {
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type RefProp,
 } from '../../utils/render'
@@ -78,6 +78,8 @@ function TextareaFn<TTag extends ElementType = typeof DEFAULT_TEXTAREA_TAG>(
     return { disabled, invalid, hover, focus, autofocus: autoFocus } satisfies TextareaRenderPropArg
   }, [disabled, invalid, hover, focus, autoFocus])
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
index 03ddacf..f464948 100644
--- a/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
+++ b/packages/@headlessui-react/src/components/tooltip/tooltip.tsx
@@ -33,7 +33,7 @@ import {
   RenderFeatures,
   forwardRefWithAs,
   mergeProps,
-  render,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -290,6 +290,8 @@ function TooltipFn<TTag extends ElementType = typeof DEFAULT_TOOLTIP_TAG>(
   )
   let actions = useMemo<_Actions>(() => ({ showTooltip, hideTooltip }), [showTooltip, hideTooltip])
 
+  let render = useRender()
+
   return (
     <DescriptionProvider value={describedBy}>
       <FloatingProvider>
@@ -395,6 +397,8 @@ function TriggerFn<TTag extends ElementType = typeof DEFAULT_TRIGGER_TAG>(
     hoverProps
   )
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
@@ -448,6 +452,8 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
 
   let slot = useMemo(() => ({}) satisfies PanelRenderPropArg, [])
 
+  let render = useRender()
+
   return render({
     ourProps: {
       ...ourProps,
diff --git a/packages/@headlessui-react/src/components/transition/transition.tsx b/packages/@headlessui-react/src/components/transition/transition.tsx
index efe0ad8..930f318 100644
--- a/packages/@headlessui-react/src/components/transition/transition.tsx
+++ b/packages/@headlessui-react/src/components/transition/transition.tsx
@@ -29,7 +29,7 @@ import {
   RenderStrategy,
   compact,
   forwardRefWithAs,
-  render,
+  useRender,
   type HasDisplayName,
   type PropsForFeatures,
   type RefProp,
@@ -475,6 +475,8 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
   if (transitionData.enter) openClosedState |= State.Opening
   if (transitionData.leave) openClosedState |= State.Closing
 
+  let render = useRender()
+
   return (
     <NestingContext.Provider value={nesting}>
       <OpenClosedProvider value={openClosedState}>
@@ -571,6 +573,8 @@ function TransitionRootFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
     props.beforeLeave?.()
   })
 
+  let render = useRender()
+
   return (
     <NestingContext.Provider value={nestingBag}>
       <TransitionContext.Provider value={transitionBag}>
diff --git a/packages/@headlessui-react/src/internal/hidden.tsx b/packages/@headlessui-react/src/internal/hidden.tsx
index e8e5b0e..fe2eb03 100644
--- a/packages/@headlessui-react/src/internal/hidden.tsx
+++ b/packages/@headlessui-react/src/internal/hidden.tsx
@@ -1,6 +1,6 @@
 import type { ElementType, Ref } from 'react'
 import type { Props } from '../types'
-import { forwardRefWithAs, render, type HasDisplayName, type RefProp } from '../utils/render'
+import { forwardRefWithAs, useRender, type HasDisplayName, type RefProp } from '../utils/render'
 
 let DEFAULT_VISUALLY_HIDDEN_TAG = 'span' as const
 
@@ -55,6 +55,8 @@ function VisuallyHidden<TTag extends ElementType = typeof DEFAULT_VISUALLY_HIDDE
     },
   }
 
+  let render = useRender()
+
   return render({
     ourProps,
     theirProps,
diff --git a/packages/@headlessui-react/src/utils/render.ts b/packages/@headlessui-react/src/utils/render.ts
index 4106c4b..ae9a6b6 100644
--- a/packages/@headlessui-react/src/utils/render.ts
+++ b/packages/@headlessui-react/src/utils/render.ts
@@ -57,7 +57,16 @@ export type PropsForFeatures<T extends RenderFeatures> = Expand<
   >
 >
 
-export function render<TFeature extends RenderFeatures, TTag extends ElementType, TSlot>({
+export function useRender() {
+  let mergeRefs = useMergeRefsFn()
+
+  return useCallback(
+    (args: Parameters<typeof render>[0]) => render({ mergeRefs, ...args }),
+    [mergeRefs]
+  ) as typeof render
+}
+
+function render<TFeature extends RenderFeatures, TTag extends ElementType, TSlot>({
   ourProps,
   theirProps,
   slot,
@@ -77,7 +86,7 @@ export function render<TFeature extends RenderFeatures, TTag extends ElementType
   visible?: boolean
   name: string
   mergeRefs?: ReturnType<typeof useMergeRefsFn>
-}) {
+}): ReturnType<typeof _render> | null {
   mergeRefs = mergeRefs ?? defaultMergeRefs
 
   let props = mergePropsAdvanced(theirProps, ourProps)
@@ -281,7 +290,7 @@ function _render<TTag extends ElementType, TSlot>(
  * the `function` that updates these refs and can only do
  * so once the ref that contains the list is updated.
  */
-export function useMergeRefsFn() {
+function useMergeRefsFn() {
   type MaybeRef<T> = MutableRefObject<T> | ((value: T) => void) | null | undefined
   let currentRefs = useRef<MaybeRef<any>[]>([])
   let mergedRef = useCallback((value: any) => {


Test patch:
diff --git a/packages/@headlessui-react/src/utils/render.test.tsx b/packages/@headlessui-react/src/utils/render.test.tsx
index 1d785b0..b6fc152 100644
--- a/packages/@headlessui-react/src/utils/render.test.tsx
+++ b/packages/@headlessui-react/src/utils/render.test.tsx
@@ -2,7 +2,7 @@ import { getByTestId, prettyDOM, render as testRender } from '@testing-library/r
 import React, { Fragment, createRef, type ElementType, type Ref } from 'react'
 import { suppressConsoleLogs } from '../test-utils/suppress-console-logs'
 import type { Expand, Props } from '../types'
-import { RenderFeatures, render, type PropsForFeatures } from './render'
+import { RenderFeatures, useRender, type PropsForFeatures } from './render'
 
 function contents(id = 'wrapper') {
   return prettyDOM(getByTestId(document.body, id), undefined, {
@@ -15,6 +15,7 @@ describe('Default functionality', () => {
   function Dummy<TTag extends ElementType = 'div'>(
     props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
   ) {
+    let render = useRender()
     return (
       <div data-testid="wrapper">
         {render({
@@ -31,6 +32,7 @@ describe('Default functionality', () => {
   function DummyWithClassName<TTag extends ElementType = 'div'>(
     props: Props<TTag> & Partial<{ className: string | (() => string) }>
   ) {
+    let render = useRender()
     return (
       <div data-testid="wrapper-with-class">
         {render({
@@ -97,6 +99,7 @@ describe('Default functionality', () => {
     }
 
     function OtherDummy<TTag extends ElementType = 'div'>(props: Props<TTag>) {
+      let render = useRender()
       return (
         <div data-testid="wrapper">
           {render({
@@ -157,6 +160,7 @@ describe('Default functionality', () => {
     function Dummy<TTag extends ElementType = 'div'>(
       props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
     ) {
+      let render = useRender()
       return (
         <div data-testid="wrapper">
           {render({
@@ -179,6 +183,7 @@ describe('Default functionality', () => {
     function Dummy<TTag extends ElementType = 'div'>(
       props: Props<TTag> & Partial<{ a: any; b: any; c: any }>
     ) {
+      let render = useRender()
       return (
         <div data-testid="wrapper">
           {render({
@@ -305,6 +310,7 @@ describe('Features.Static', () => {
     props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
   ) {
     let { show, ...rest } = props
+    let render = useRender()
     return (
       <div data-testid="wrapper">
         {render({
@@ -380,6 +386,7 @@ describe('Features.RenderStrategy', () => {
     props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
   ) {
     let { show, ...rest } = props
+    let render = useRender()
     return (
       <div data-testid="wrapper">
         {render({
@@ -408,6 +415,7 @@ describe('Features.Static | Features.RenderStrategy', () => {
     props: Expand<Props<TTag> & PropsForFeatures<typeof EnabledFeatures>> & { show: boolean }
   ) {
     let { show, ...rest } = props
+    let render = useRender()
     return (
       <div data-testid="wrapper">
         {render({