useRemoteTrigger
Control dialogs, sheets, and other components triggered by open states or on the click of a trigger.
The useRemoteTrigger
hook provides a simple way to control dialogs, sheets, or similar components using either local state (clicking a trigger) or external props (open
and onOpenChange
).
It internally manages isOpen
state and keeps it synchronized with the open
prop via useEffect
. Whenever open
changes externally, the hook updates the internal state to reflect it. The returned handleOpenChange
callback updates both the internal state and calls onOpenChange
, ensuring external and internal state stay in sync.
This dual approach allows you to build components that:
- Work as uncontrolled components with internal state (e.g., opened by a button).
- Switch to controlled mode when
open
andonOpenChange
are provided by a parent.
By returning [isOpen, handleOpenChange]
, the hook makes it easy to integrate with libraries like Radix UI that expect open
and onOpenChange
, enabling flexible and predictable state management without extra boilerplate.
Code
interface UseRemoteTriggerProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
export interface RemoteTriggerProps extends UseRemoteTriggerProps {
children?: React.ReactNode;
}
export const useRemoteTrigger = ({
open,
onOpenChange,
}: UseRemoteTriggerProps) => {
const [isOpen, setIsOpen] = useState(open);
useEffect(() => {
setIsOpen(open);
}, [open]);
const handleOpenChange = useCallback(
(open: boolean) => {
setIsOpen(open);
onOpenChange?.(open);
},
[onOpenChange],
);
return [isOpen, handleOpenChange] as const;
};
Example
The first dialog is controlled via a trigger (button), and the second is controlled remotely, but they are the same component.
Usage
const ExampleTriggeredDialog = ({
children,
open,
onOpenChange,
}: RemoteTriggerProps) => {
if (!children && !open && !onOpenChange) {
throw new Error(
'A dialog must be opened by a child as a trigger or by states',
);
}
const [isOpen, handleOpenChange] = useRemoteTrigger({
open,
onOpenChange,
});
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{children && <DialogTrigger asChild>{children}</DialogTrigger>}
<DialogContent>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog Description</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
};