Hooks
Migration guide for HeroUI hooks from v2 to v3
Refer to the v3 component documentation for complete API reference. This guide focuses on migrating hooks from HeroUI v2.
Overview
HeroUI v3 removes most component hooks that existed in v2, replacing them with compound components and a new hook for overlay state management. This guide covers:
- Component hooks removal (useSwitch, useInput, useCheckbox, etc.)
- useDisclosure → useOverlayState migration
- Migration strategies and examples
Component Hooks Removal
HeroUI v2 provided component hooks (like useSwitch, useInput, useCheckbox, etc.) that returned prop getters (getBaseProps, getWrapperProps, getThumbProps, etc.) to customize component structure when users couldn't directly modify inner child components. HeroUI v3 solves this with compound components, eliminating the need for hooks.
Why Hooks Existed in v2
In v2, components had fixed internal structures. To customize these structures, users needed to use hooks that provided prop getters. For example, useSwitch returned getBaseProps(), getWrapperProps(), getThumbProps(), etc., which users could spread onto custom elements to build their own Switch structure.
v3 Solution: Compound Components
v3 uses compound component patterns that give you direct access to component parts. Instead of using hooks with prop getters, you compose components directly using subcomponents like Switch.Control, Switch.Thumb, Checkbox.Control, Checkbox.Indicator, etc.
Migration Strategy
- Identify hook usage: Search your codebase for imports from
@heroui/reactthat include hook names (useSwitch,useInput,useCheckbox,useRadio, etc.) - Replace with compound components: Instead of using hooks with prop getters, use the compound component pattern
- Preserve original structure: When migrating, try to keep the same component structure you had with hooks. For example:
- If you used
useSwitchto create a switch without a thumb, don't addSwitch.Thumbin v3 - If you used
useCheckboxto create a checkbox without an indicator, don't addCheckbox.Indicatorin v3 - Only include the subcomponents that were actually used in your hook-based implementation
- If you used
- Reference component guides: Check individual component migration guides for specific examples
Key Differences
- v2: Hooks provided prop getters to customize fixed component structures
- v3: Compound components allow direct composition of component parts
Preserving Structure Example
v2: Switch without thumb
import { useSwitch } from "@heroui/react";
function CustomSwitch() {
const { getBaseProps } = useSwitch();
return (
<div {...getBaseProps()}>
{/* No thumb element */}
</div>
);
}v3: Equivalent structure
import { Switch } from "@heroui/react";
function CustomSwitch() {
return (
<Switch.Control>
{/* No Switch.Thumb - preserving the original structure */}
</Switch.Control>
);
}For detailed migration examples for specific components, see the individual component migration guides.
useDisclosure → useOverlayState
The useDisclosure hook from v2 has been replaced with useOverlayState in v3. This hook manages open/close state for modals, popovers, and other overlay components.
v2: useDisclosure
API:
const {isOpen, onOpen, onClose, onOpenChange, isControlled, getButtonProps, getDisclosureProps} = useDisclosure({
isOpen?: boolean;
defaultOpen?: boolean;
onClose?(): void;
onOpen?(): void;
onChange?(isOpen: boolean | undefined): void;
id?: string;
});Example:
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, useDisclosure } from "@heroui/react";
export default function App() {
const {isOpen, onOpen, onOpenChange} = useDisclosure();
return (
<>
<Button onPress={onOpen}>Open Modal</Button>
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent>
<ModalHeader>Title</ModalHeader>
<ModalBody>Content</ModalBody>
<ModalFooter>
<Button onPress={onOpenChange}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}v3: useOverlayState
API:
const state = useOverlayState({
isOpen?: boolean;
defaultOpen?: boolean;
onOpenChange?: (isOpen: boolean) => void;
});
// Returns:
// {
// isOpen: boolean;
// open(): void;
// close(): void;
// toggle(): void;
// setOpen(isOpen: boolean): void;
// }Example:
import { Modal, Button, useOverlayState } from "@heroui/react";
export default function App() {
const state = useOverlayState();
return (
<Modal>
<Button onPress={state.open}>Open Modal</Button>
<Modal.Container isOpen={state.isOpen} onOpenChange={state.setOpen}>
<Modal.Dialog>
{({close}) => (
<>
<Modal.Header>
<Modal.Heading>Title</Modal.Heading>
</Modal.Header>
<Modal.Body>Content</Modal.Body>
<Modal.Footer>
<Button onPress={close}>Close</Button>
</Modal.Footer>
</>
)}
</Modal.Dialog>
</Modal.Container>
</Modal>
);
}Migration Guide
Basic Migration
v2:
const {isOpen, onOpen, onClose, onOpenChange} = useDisclosure();v3:
const state = useOverlayState();
// Use state.open(), state.close(), state.toggle(), state.setOpen(boolean)Controlled State
v2:
const {isOpen, onOpenChange} = useDisclosure({
isOpen: controlledIsOpen,
onChange: (isOpen) => setControlledIsOpen(isOpen)
});v3:
const state = useOverlayState({
isOpen: controlledIsOpen,
onOpenChange: setControlledIsOpen
});Uncontrolled State
v2:
const {isOpen, onOpen, onClose} = useDisclosure({
defaultOpen: false
});v3:
const state = useOverlayState({
defaultOpen: false
});
// Use state.open(), state.close(), state.toggle()API Differences
| v2 (useDisclosure) | v3 (useOverlayState) | Notes |
|---|---|---|
isOpen | isOpen | Same |
onOpen() | open() | Renamed method |
onClose() | close() | Renamed method |
onOpenChange() | toggle() | New method for toggling |
onOpenChange (prop) | setOpen(boolean) | Different API |
isControlled | - | Removed (handled internally) |
getButtonProps() | - | Removed (use compound components) |
getDisclosureProps() | - | Removed (use compound components) |
Benefits of useOverlayState
- Cleaner API: Dedicated methods (
open(),close(),toggle()) instead of callbacks - Simpler state management: Works seamlessly with both controlled and uncontrolled patterns
- Better TypeScript support: Improved type inference and autocomplete
- Consistent with React Aria: Aligns with React Aria Components patterns
Alternative: useState
For simple cases, you can also use React's useState directly:
import { useState } from "react";
import { Modal, Button } from "@heroui/react";
export default function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<Modal>
<Button onPress={() => setIsOpen(true)}>Open</Button>
<Modal.Container isOpen={isOpen} onOpenChange={setIsOpen}>
<Modal.Dialog>
{/* content */}
</Modal.Dialog>
</Modal.Container>
</Modal>
);
}However, useOverlayState provides a cleaner API with dedicated methods for common operations.
Removed Hooks
The following hooks from v2 have been removed in v3:
- useDraggable: Removed
- useClipboard: Removed
- usePagination: Removed
- useToast: Removed
Summary
- Component hooks (
useSwitch,useInput, etc.) → Use compound components instead - useDisclosure → Use useOverlayState for overlay state management
- useOverlayState provides a cleaner API with
open(),close(),toggle(), andsetOpen()methods - Removed hooks:
useDraggable,useClipboard,usePagination,useToastare no longer available - For simple cases,
useStatecan be used directly, butuseOverlayStateoffers better ergonomics
For component-specific hook migration examples, refer to the individual component migration guides.