Skip to content

Commit 9dc83e0

Browse files
authored
Fix 'Invalid prop data-headlessui-state supplied to React.Fragment' warning (#3788)
This PR fixes an issue in React 19 where a warning is shown in the console: ``` Invalid prop `data-headlessui-state` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props. ``` This PR fixes that by doing 2 things: 1. We make sure that we properly check for `Fragment`. We were relying on identity, but will now also check the underlying symbol. 2. Make sure that the element we forward props to is not a `Fragment` either. ## Test plan - All existing tests still pass (this codebase is using React 18 instead of 19, where we can't reproduce the warning) - Tested in a reproduction repo with React 19 that the warning is gone. Before: <img width="1190" height="849" alt="image" src="https://github.com/user-attachments/assets/c2fd061c-4b6b-4685-a59c-c0edc6eb8642" /> After: <img width="1190" height="849" alt="image" src="https://github.com/user-attachments/assets/ee8318b6-06fa-4fa7-84bf-b96af3a34f87" /> Fixes: #3597 Fixes: #3351
1 parent c9bf352 commit 9dc83e0

File tree

4 files changed

+18
-8
lines changed

4 files changed

+18
-8
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Fix `Maximum update depth exceeded` crash when using `transition` prop ([#3782](https://github.com/tailwindlabs/headlessui/pull/3782))
1616
- Ensure pressing `Tab` in the `ComboboxInput`, correctly syncs the input value ([#3785](https://github.com/tailwindlabs/headlessui/pull/3785))
1717
- Ensure `--button-width` and `--input-width` have the latest value ([#3786](https://github.com/tailwindlabs/headlessui/pull/3786))
18+
- Fix 'Invalid prop `data-headlessui-state` supplied to `React.Fragment`' warning ([#3788](https://github.com/tailwindlabs/headlessui/pull/3788))
1819

1920
## [2.2.7] - 2025-07-30
2021

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { getOwnerDocument } from '../../utils/owner'
4141
import {
4242
RenderFeatures,
4343
forwardRefWithAs,
44+
isFragment,
4445
mergeProps,
4546
useRender,
4647
type HasDisplayName,
@@ -188,9 +189,7 @@ function DisclosureFn<TTag extends ElementType = typeof DEFAULT_DISCLOSURE_TAG>(
188189
(ref) => {
189190
internalDisclosureRef.current = ref
190191
},
191-
props.as === undefined ||
192-
// @ts-expect-error The `as` prop _can_ be a Fragment
193-
props.as === Fragment
192+
props.as === undefined || isFragment(props.as)
194193
)
195194
)
196195

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
RenderStrategy,
3030
compact,
3131
forwardRefWithAs,
32+
isFragment,
3233
useRender,
3334
type HasDisplayName,
3435
type PropsForFeatures,
@@ -71,7 +72,7 @@ function shouldForwardRef<TTag extends ElementType = typeof DEFAULT_TRANSITION_C
7172
props.leaveTo
7273
) ||
7374
// If the `as` prop is not a Fragment
74-
(props.as ?? DEFAULT_TRANSITION_CHILD_TAG) !== Fragment ||
75+
!isFragment(props.as ?? DEFAULT_TRANSITION_CHILD_TAG) ||
7576
// If we have a single child, then we can forward the ref directly
7677
React.Children.count(props.children) === 1
7778
)

packages/@headlessui-react/src/utils/render.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,12 @@ function _render<TTag extends ElementType, TSlot>(
181181
}
182182
}
183183

184-
if (Component === Fragment) {
184+
if (isFragment(Component)) {
185185
if (Object.keys(compact(rest)).length > 0 || Object.keys(compact(dataAttributes)).length > 0) {
186186
if (
187187
!isValidElement(resolvedChildren) ||
188-
(Array.isArray(resolvedChildren) && resolvedChildren.length > 1)
188+
(Array.isArray(resolvedChildren) && resolvedChildren.length > 1) ||
189+
isFragmentInstance(resolvedChildren)
189190
) {
190191
if (Object.keys(compact(rest)).length > 0) {
191192
throw new Error(
@@ -270,8 +271,8 @@ function _render<TTag extends ElementType, TSlot>(
270271
Object.assign(
271272
{},
272273
omit(rest, ['ref']),
273-
Component !== Fragment && refRelatedProps,
274-
Component !== Fragment && dataAttributes
274+
!isFragment(Component) && refRelatedProps,
275+
!isFragment(Component) && dataAttributes
275276
),
276277
resolvedChildren
277278
)
@@ -465,3 +466,11 @@ function getElementRef(element: React.ReactElement) {
465466
// @ts-expect-error
466467
return React.version.split('.')[0] >= '19' ? element.props.ref : element.ref
467468
}
469+
470+
export function isFragment(element: any): element is typeof Fragment {
471+
return element === Fragment || element === Symbol.for('react.fragment')
472+
}
473+
474+
export function isFragmentInstance(element: React.ReactElement): boolean {
475+
return isFragment(element.type)
476+
}

0 commit comments

Comments
 (0)