All files / hooks/use-disclosure/src useDisclosure.ts

100% Statements 24/24
100% Branches 8/8
100% Functions 6/6
100% Lines 24/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102                                                                                                              28x   28x         28x 28x 26x     28x 28x   28x 9x 7x 7x 7x       28x 9x 6x 6x 6x       28x 5x 3x   2x       28x 13x       28x    
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { UseDisclosureOptions, UseDisclosureReturn } from "./types";
import { useCallbackRef } from "./utils";
 
/**
 * Manage a boolean "open" state with `open` / `close` / `toggle` handlers — the
 * ergonomic primitive for modals, drawers, popovers, dropdowns, and accordions.
 *
 * Returns a `[opened, handlers]` tuple (the Mantine shape). The handlers have
 * **stable identities**, so passing them to memoized children or listing them in
 * effect dependencies never causes extra work. Optional `onOpen` / `onClose`
 * callbacks fire only on a *real* transition (not when already in that state).
 *
 * @param initialState - Whether it starts open. Defaults to `false`.
 * @param options - Optional `{ onOpen, onClose }` transition callbacks.
 * @returns `[opened, { open, close, toggle }]`.
 *
 * @example
 * ```tsx
 * function Example() {
 *   const [opened, { open, close, toggle }] = useDisclosure(false);
 *
 *   return (
 *     <>
 *       <button onClick={open}>Open</button>
 *       <button onClick={toggle}>Toggle</button>
 *       {opened && (
 *         <div role="dialog">
 *           Modal content
 *           <button onClick={close}>Close</button>
 *         </div>
 *       )}
 *     </>
 *   );
 * }
 * ```
 *
 * @example
 * ```tsx
 * // React to transitions (analytics, focus management, …).
 * const [opened, handlers] = useDisclosure(false, {
 *   onOpen: () => trackEvent("drawer_opened"),
 *   onClose: () => trackEvent("drawer_closed"),
 * });
 * ```
 *
 * @remarks
 * `onOpen` / `onClose` are dispatched from the event handler after the state
 * change is requested — never from inside a `setState` updater — so they are
 * safe under React StrictMode / concurrent rendering and never double-fire.
 */
export function useDisclosure(
  initialState = false,
  options: UseDisclosureOptions = {}
): UseDisclosureReturn {
  const { onOpen, onClose } = options;
 
  const [opened, setOpened] = useState(initialState);
 
  // Mirror the committed state so the handlers can read the latest value
  // synchronously (and decide whether a transition actually happens) without
  // running side effects inside a setState updater.
  const openedRef = useRef(opened);
  useEffect(() => {
    openedRef.current = opened;
  }, [opened]);
 
  const handleOpen = useCallbackRef(onOpen);
  const handleClose = useCallbackRef(onClose);
 
  const open = useCallback(() => {
    if (!openedRef.current) {
      openedRef.current = true;
      setOpened(true);
      handleOpen();
    }
  }, [handleOpen]);
 
  const close = useCallback(() => {
    if (openedRef.current) {
      openedRef.current = false;
      setOpened(false);
      handleClose();
    }
  }, [handleClose]);
 
  const toggle = useCallback(() => {
    if (openedRef.current) {
      close();
    } else {
      open();
    }
  }, [open, close]);
 
  const handlers = useMemo(
    () => ({ open, close, toggle }),
    [open, close, toggle]
  );
 
  return [opened, handlers] as const;
}