Skip to Content
HooksuseDisclosure

useDisclosure

A simple and elegant hook for managing boolean state with common actions like open, close, and toggle. Perfect for controlling modals, dropdowns, sidebars, and any UI component that needs show/hide functionality.


When to use

  • Modal management: Control modal open/close state
  • Dropdown menus: Manage dropdown visibility
  • Sidebar navigation: Toggle sidebar open/closed
  • Accordion panels: Control expand/collapse state
  • Tooltip visibility: Show/hide tooltips on demand
  • Form validation: Show/hide error messages
  • Loading states: Toggle loading indicators

Parameters

ParameterTypeDefaultDescription
menuOpenedbooleanfalseInitial boolean state value

Return Values

PropertyTypeDescription
openedbooleanCurrent boolean state
open() => voidFunction to set state to true
close() => voidFunction to set state to false
toggle() => voidFunction to toggle current state

Basic Usage

import { useDisclosure } from "@kousta-ui/hooks"; function BasicExample() { const { opened, open, close, toggle } = useDisclosure(false); return ( <div> <button onClick={open}>Open</button> <button onClick={close}>Close</button> <button onClick={toggle}>Toggle</button> <p>Current state: {opened ? "opened" : "closed"}</p> </div> ); }

Examples

function ModalExample() { const { opened, open, close } = useDisclosure(); return ( <div> <button onClick={open}>Open Modal</button> {opened && ( <div className="modal-overlay"> <div className="modal"> <h2>Modal Title</h2> <p>This is a modal controlled by useDisclosure.</p> <button onClick={close}>Close Modal</button> </div> </div> )} </div> ); }
function DropdownExample() { const { opened, open, close, toggle } = useDisclosure(); const dropdownRef = useRef<HTMLDivElement>(null); // Close dropdown when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { close(); } }; if (opened) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [opened, close]); return ( <div ref={dropdownRef} className="dropdown"> <button onClick={toggle}>Menu</button> {opened && ( <div className="dropdown-menu"> <div className="menu-item">Profile</div> <div className="menu-item">Settings</div> <div className="menu-item">Logout</div> </div> )} </div> ); }
function SidebarExample() { const { opened, open, close, toggle } = useDisclosure(); return ( <div className={`app ${opened ? "sidebar-open" : ""}`}> <button className="menu-toggle" onClick={toggle}> </button> <aside className={`sidebar ${opened ? "open" : ""}`}> <nav> <a href="#home">Home</a> <a href="#about">About</a> <a href="#contact">Contact</a> </nav> </aside> <main className="content"> <h1>Main Content</h1> <p>This is the main content area.</p> </main> </div> ); }

Accordion Component

function AccordionExample() { const [panels, setPanels] = useState({ panel1: false, panel2: false, panel3: false, }); const createDisclosure = (key: keyof typeof panels) => ({ opened: panels[key], open: () => setPanels(prev => ({ ...prev, [key]: true })), close: () => setPanels(prev => ({ ...prev, [key]: false })), toggle: () => setPanels(prev => ({ ...prev, [key]: !prev[key] })), }); const panel1 = createDisclosure("panel1"); const panel2 = createDisclosure("panel2"); const panel3 = createDisclosure("panel3"); return ( <div className="accordion"> <div className="accordion-item"> <button onClick={panel1.toggle}> Panel 1 {panel1.opened ? "▼" : "▶"} </button> {panel1.opened && ( <div className="accordion-content"> Content for panel 1 goes here. </div> )} </div> <div className="accordion-item"> <button onClick={panel2.toggle}> Panel 2 {panel2.opened ? "▼" : "▶"} </button> {panel2.opened && ( <div className="accordion-content"> Content for panel 2 goes here. </div> )} </div> <div className="accordion-item"> <button onClick={panel3.toggle}> Panel 3 {panel3.opened ? "▼" : "▶"} </button> {panel3.opened && ( <div className="accordion-content"> Content for panel 3 goes here. </div> )} </div> </div> ); }

Form Validation Messages

function FormExample() { const [email, setEmail] = useState(""); const [errors, setErrors] = useState<Record<string, string>>({}); const emailError = useDisclosure(false); const passwordError = useDisclosure(false); const validateEmail = (value: string) => { const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); if (!isValid) { setErrors(prev => ({ ...prev, email: "Please enter a valid email" })); emailError.open(); } else { setErrors(prev => ({ ...prev, email: "" })); emailError.close(); } }; return ( <form> <div> <label>Email</label> <input type="email" value={email} onChange={(e) => { setEmail(e.target.value); validateEmail(e.target.value); }} /> {emailError.opened && ( <span className="error">{errors.email}</span> )} </div> <div> <label>Password</label> <input type="password" onChange={(e) => { if (e.target.value.length < 8) { passwordError.open(); } else { passwordError.close(); } }} /> {passwordError.opened && ( <span className="error">Password must be at least 8 characters</span> )} </div> </form> ); }

Loading States

function LoadingExample() { const { opened: isLoading, open: startLoading, close: stopLoading } = useDisclosure(); const handleSubmit = async () => { startLoading(); try { // Simulate API call await new Promise(resolve => setTimeout(resolve, 2000)); console.log("Form submitted successfully!"); } catch (error) { console.error("Submission failed:", error); } finally { stopLoading(); } }; return ( <div> <button onClick={handleSubmit} disabled={isLoading}> {isLoading ? "Submitting..." : "Submit Form"} </button> {isLoading && ( <div className="loading-overlay"> <div className="spinner"></div> <p>Please wait...</p> </div> )} </div> ); }

Advanced Patterns

Multiple Disclosure States

function MultiDisclosureExample() { const disclosures = { modal: useDisclosure(), dropdown: useDisclosure(), sidebar: useDisclosure(), }; return ( <div> <button onClick={disclosures.modal.open}>Open Modal</button> <button onClick={disclosures.dropdown.toggle}>Toggle Dropdown</button> <button onClick={disclosures.sidebar.open}>Open Sidebar</button> {/* Close all at once */} <button onClick={() => { Object.values(disclosures).forEach(disclosure => disclosure.close()); }}> Close All </button> </div> ); }

Disclosure with Effects

function DisclosureWithEffects() { const { opened, open, close } = useDisclosure(); // Effect when state changes useEffect(() => { if (opened) { console.log("Component opened"); document.body.style.overflow = "hidden"; } else { console.log("Component closed"); document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [opened]); return ( <div> <button onClick={open}>Open</button> <button onClick={close}>Close</button> <p>State: {opened ? "Open" : "Closed"}</p> </div> ); }

Performance Considerations

  • Lightweight: Minimal overhead compared to manual state management
  • Stable Functions: open, close, and toggle functions are stable across re-renders
  • No Dependencies: Zero external dependencies for optimal bundle size

Performance Tip The hook uses useState internally, so it follows React’s optimization patterns automatically.


TypeScript Support

Full TypeScript support with generic inference:

// Basic usage const { opened, open, close, toggle } = useDisclosure(false); // With explicit typing const disclosure: { opened: boolean; open: () => void; close: () => void; toggle: () => void; } = useDisclosure(); // In custom hook function useCustomModal() { const disclosure = useDisclosure(false); return { ...disclosure, // Add custom methods openWithDelay: () => setTimeout(disclosure.open, 100), }; }

Common Pitfalls

Don’t Mutate Return Values

// ❌ Wrong - this won't work const { opened } = useDisclosure(); opened = true; // Error: Cannot assign to 'opened' because it is a read-only property // ✅ Correct - use the provided functions const { opened, open, close } = useDisclosure(); open(); // or close(), or toggle()

Don’t Use Outside Components

// ❌ Wrong - hooks must be called in components const { open } = useDisclosure(); // This will cause an error function Component() { // ✅ Correct - use inside components const { open } = useDisclosure(); return <button onClick={open}>Open</button>; }

Migration from Manual State

Before Manual State

function OldComponent() { const [isModalOpen, setIsModalOpen] = useState(false); const openModal = () => setIsModalOpen(true); const closeModal = () => setIsModalOpen(false); const toggleModal = () => setIsModalOpen(prev => !prev); return ( <div> <button onClick={openModal}>Open</button> <button onClick={closeModal}>Close</button> <button onClick={toggleModal}>Toggle</button> {isModalOpen && <div>Modal Content</div>} </div> ); }

After useDisclosure

function NewComponent() { const { opened, open, close, toggle } = useDisclosure(); return ( <div> <button onClick={open}>Open</button> <button onClick={close}>Close</button> <button onClick={toggle}>Toggle</button> {opened && <div>Modal Content</div>} </div> ); }

Types (reference)

export function useDisclosure( initialState?: boolean ): { opened: boolean; open: () => void; close: () => void; toggle: () => void; };
Last updated on