I have a Button
component that allows some props like variant
, active
, size
etc. to change its styling.
What I want is, passing the Button component to another component and use it there as a normal component.
function Header() {
return (
<div>
<Button.Link href="/">Log in</Button.Link>
<Button.Link href="/">Sign up</Button.Link>
<Dropdown
Trigger={
<Button variant="outline" size="icon">
<MenuIcon className="size-4" />
</Button>
}
>
<div className="absolute right-0 top-8 h-72 w-64 bg-red-200">Hello</div>
</Dropdown>
</div>
);
}
export default Header;
As you can see I am passing the Button
component to the Dropdown
component.
Now in Dropdown, I want to use it as shown below: (dummy code for explanation)
interface DropdownProps {
Trigger: any;
children: React.ReactNode;
}
function Dropdown({ Trigger, children }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const toggleDropdown = () => {
setIsOpen((current) => !current);
};
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef} className="relative">
{<Trigger onClick={toggleDropdown} active={isOpen} />} <-- like this
{isOpen && children}
</div>
);
}
export default Dropdown;
How can I achieve this functionality?
What I have done:
I found a solution using React's cloneElement
but not happy with it (feels like there is a better way).
interface DropdownProps {
trigger: React.ReactElement<{
onClick: () => void;
active: boolean;
}>;
children: React.ReactNode;
}
function Dropdown({ trigger, children }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const toggleDropdown = () => {
setIsOpen((current) => !current);
};
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef} className="relative">
{cloneElement(trigger, { onClick: toggleDropdown, active: isOpen })}
{isOpen && children}
</div>
);
}
export default Dropdown;
Anything better?
A better approach to manage props and ensure better type safety is to define Trigger as a function component that receives necessary props and events directly from Dropdown. This approach avoids React.cloneElement, making the code cleaner and providing better type safety.
Here’s how to implement it by using a Trigger function component that receives onClick and active props from Dropdown.
import React, { useState, useRef } from 'react';
interface DropdownProps {
Trigger: (props: { onClick: () => void; active: boolean }) => JSX.Element;
children: React.ReactNode;
}
function Dropdown({ Trigger, children }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const toggleDropdown = () => {
setIsOpen((current) => !current);
};
useClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef} className="relative">
<Trigger onClick={toggleDropdown} active={isOpen} />
{isOpen && children}
</div>
);
}
export default Dropdown;
Now in Header, you can pass Trigger as a function and use it with the Button component:
function Header() {
return (
<div>
<Button.Link href="/">Log in</Button.Link>
<Button.Link href="/">Sign up</Button.Link>
<Dropdown
Trigger={({ onClick, active }) => (
<Button variant="outline" size="icon" onClick={onClick} active={active}>
<MenuIcon className="size-4" />
</Button>
)}
>
<div className="absolute right-0 top-8 h-72 w-64 bg-red-200">Hello</div>
</Dropdown>
</div>
);
}
I hope it helps you.