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
openandonOpenChangeare 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>
);
};