Pro--% off in--d : --h : --m : --s
HeroUI

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

  1. Identify hook usage: Search your codebase for imports from @heroui/react that include hook names (useSwitch, useInput, useCheckbox, useRadio, etc.)
  2. Replace with compound components: Instead of using hooks with prop getters, use the compound component pattern
  3. Preserve original structure: When migrating, try to keep the same component structure you had with hooks. For example:
    • If you used useSwitch to create a switch without a thumb, don't add Switch.Thumb in v3
    • If you used useCheckbox to create a checkbox without an indicator, don't add Checkbox.Indicator in v3
    • Only include the subcomponents that were actually used in your hook-based implementation
  4. 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
isOpenisOpenSame
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(), and setOpen() methods
  • Removed hooks: useDraggable, useClipboard, usePagination, useToast are no longer available
  • For simple cases, useState can be used directly, but useOverlayState offers better ergonomics

For component-specific hook migration examples, refer to the individual component migration guides.

On this page