diff --git a/.changeset/purple-donuts-smell.md b/.changeset/purple-donuts-smell.md
new file mode 100644
index 000000000..3e5236659
--- /dev/null
+++ b/.changeset/purple-donuts-smell.md
@@ -0,0 +1,5 @@
+---
+'@radix-ui/react-slot': patch
+---
+
+This changeset patches an issue with how slot components interact with lazy React components. In the case of a lazy component instance, the resulting promise must be consumed to render the desired component.
diff --git a/packages/react/slot/src/__snapshots__/slot.test.tsx.snap b/packages/react/slot/src/__snapshots__/slot.test.tsx.snap
index 4f7692486..11d7b7f56 100644
--- a/packages/react/slot/src/__snapshots__/slot.test.tsx.snap
+++ b/packages/react/slot/src/__snapshots__/slot.test.tsx.snap
@@ -35,3 +35,22 @@ exports[`given a Button with Slottable > without asChild > should render a butto
`;
+
+exports[`given a Slot with React lazy components > with a lazy component in Button with Slottable > should render a lazy link with icon on the left/right 1`] = `
+
+`;
diff --git a/packages/react/slot/src/slot.test.tsx b/packages/react/slot/src/slot.test.tsx
index b7fae1389..d3f777078 100644
--- a/packages/react/slot/src/slot.test.tsx
+++ b/packages/react/slot/src/slot.test.tsx
@@ -172,6 +172,65 @@ describe.skip('given an Input', () => {
});
});
+describe('given a Slot with React lazy components', () => {
+ afterEach(cleanup);
+
+ describe('with a lazy component as child', () => {
+ const LazyButton = React.lazy(() =>
+ Promise.resolve({
+ default: ({ children, ...props }: React.ComponentProps<'button'>) => (
+ {children}
+ ),
+ })
+ );
+
+ it('should render the lazy component correctly', async () => {
+ const handleClick = vi.fn();
+
+ render(
+ Loading...}>
+
+ Click me
+
+
+ );
+
+ // Wait for lazy component to load
+ await screen.findByRole('button');
+
+ fireEvent.click(screen.getByRole('button'));
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('with a lazy component in Button with Slottable', () => {
+ const LazyLink = React.lazy(() =>
+ Promise.resolve({
+ default: ({ children, ...props }: React.ComponentProps<'a'>) => (
+ {children}
+ ),
+ })
+ );
+
+ it('should render a lazy link with icon on the left/right', async () => {
+ const tree = render(
+ Loading...}>
+ left} iconRight={right } asChild>
+
+ Button text
+
+
+
+ );
+
+ // Wait for lazy component to load
+ await screen.findByRole('link');
+
+ expect(tree.container).toMatchSnapshot();
+ });
+ });
+});
+
type TriggerProps = React.ComponentProps<'button'> & { as: React.ElementType };
const Trigger = ({ as: Comp = 'button', ...props }: TriggerProps) => ;
diff --git a/packages/react/slot/src/slot.tsx b/packages/react/slot/src/slot.tsx
index 7e31d6654..2b582bfdb 100644
--- a/packages/react/slot/src/slot.tsx
+++ b/packages/react/slot/src/slot.tsx
@@ -1,6 +1,19 @@
import * as React from 'react';
import { composeRefs } from '@radix-ui/react-compose-refs';
+declare module 'react' {
+ interface ReactElement {
+ $$typeof?: symbol | string;
+ }
+}
+
+const REACT_LAZY_TYPE = Symbol.for('react.lazy');
+
+interface LazyReactElement extends React.ReactElement {
+ $$typeof: typeof REACT_LAZY_TYPE;
+ _payload: any;
+}
+
/* -------------------------------------------------------------------------------------------------
* Slot
* -----------------------------------------------------------------------------------------------*/
@@ -9,10 +22,18 @@ interface SlotProps extends React.HTMLAttributes {
children?: React.ReactNode;
}
+function isLazyComponent(element: React.ReactNode): element is LazyReactElement {
+ // has to be done in a roundabout way unless we want to add a dependency on react-is
+ return React.isValidElement(element) && element.$$typeof === REACT_LAZY_TYPE;
+}
+
/* @__NO_SIDE_EFFECTS__ */ export function createSlot(ownerName: string) {
const SlotClone = createSlotClone(ownerName);
const Slot = React.forwardRef((props, forwardedRef) => {
- const { children, ...slotProps } = props;
+ let { children, ...slotProps } = props;
+ if (isLazyComponent(children)) {
+ children = React.use(children._payload);
+ }
const childrenArray = React.Children.toArray(children);
const slottable = childrenArray.find(isSlottable);
@@ -65,7 +86,10 @@ interface SlotCloneProps {
/* @__NO_SIDE_EFFECTS__ */ function createSlotClone(ownerName: string) {
const SlotClone = React.forwardRef((props, forwardedRef) => {
- const { children, ...slotProps } = props;
+ let { children, ...slotProps } = props;
+ if (isLazyComponent(children)) {
+ children = React.use(children._payload);
+ }
if (React.isValidElement(children)) {
const childrenRef = getElementRef(children);