UNPKG

@loke/ui

Version:
292 lines (183 loc) 9.28 kB
# Hooks — Complete Reference All imports use subpath exports: `@loke/ui/<hook-name>`. --- ## useControllableState **Import:** `@loke/ui/use-controllable-state` **File:** `src/hooks/use-controllable-state/controllable-state.tsx` ### Signature ```tsx function useControllableState<T>(params: { prop?: T | undefined; // controlled value; undefined = uncontrolled defaultProp: T; // initial value for uncontrolled mode onChange?: (state: T) => void; // fires on every value change in either mode caller?: string; // component name for dev warning messages }): [T, Dispatch<SetStateAction<T>>] ``` ### Behavior - When `prop` is `undefined`: internal `useState` manages state. `setX` updates it and calls `onChange`. - When `prop` is defined: no internal state. `setX` calls `onChange(nextValue)` only if value differs. Parent must update `prop` for re-render. - In development, warns (via `console.warn`) if `prop` transitions between `undefined` and a value after mount. - The returned setter accepts both `T` and `(prev: T) => T` (same as React's `setState`). ### Notes `onChange` is stored in a ref via `useInsertionEffect` so it is always the latest version without being a dependency of the setter's `useCallback`. --- ## useCallbackRef **Import:** `@loke/ui/use-callback-ref` **File:** `src/hooks/use-callback-ref/callback-ref.ts` ### Signature ```tsx function useCallbackRef<T extends (...args: any[]) => any>( callback: T | undefined, ): T ``` ### Behavior Returns a stable function that always delegates to the latest `callback`. The stable function is created once via `useMemo([], [])`. The ref is updated after every render via `useEffect` (no deps array), so it always holds the latest value. ### When to use - Passing a callback prop to `useEffect` without adding it to the deps array. - Storing event handlers in long-lived data structures (maps, closures) that should not re-create on each render. - Any scenario where you need a stable identity but always-fresh behavior. ### When NOT to use Do not use `useCallbackRef` to memoize expensive computations — it calls the latest callback every invocation. For computation memoization, use `useMemo`. --- ## useDirection / DirectionProvider **Import:** `@loke/ui/use-direction` **File:** `src/hooks/use-direction/direction.tsx` ### Signature ```tsx function useDirection(localDir?: "ltr" | "rtl"): "ltr" | "rtl" interface DirectionProviderProps { dir: "ltr" | "rtl"; children?: React.ReactNode; } const DirectionProvider: FC<DirectionProviderProps> ``` ### Behavior `useDirection` resolves direction in priority order: 1. `localDir` argument (component-level override) 2. Nearest `DirectionProvider` in the tree 3. `"ltr"` (hardcoded fallback) `DirectionProvider` uses a standard `React.createContext` (not the library's `createContext`) — it does not throw on missing provider. ### Usage notes - Set at the application root based on `document.documentElement.dir` or a user preference. - Popper reads direction from `dir` prop on `PopperContent` for Floating UI's logical alignment calculations — always pass `dir` through to portaled content. - RTL affects Popper's `start`/`end` alignment interpretation. --- ## useEscapeKeydown **Import:** `@loke/ui/use-escape-keydown` **File:** `src/hooks/use-escape-keydown/escape-keydown.tsx` ### Signature ```tsx function useEscapeKeydown( onEscapeKeyDown?: (event: KeyboardEvent) => void, ownerDocument?: Document, // default: globalThis.document ): void ``` ### Behavior Registers a `keydown` listener on `ownerDocument` in **capture phase** (`{ capture: true }`). Fires the callback when `event.key === "Escape"`. Uses `useCallbackRef` internally so the callback is always fresh without re-registering the listener. ### Notes - Capture phase ensures Escape is caught before bubbling. DismissableLayer uses this directly and checks whether the layer is the topmost before calling `onDismiss`. - Pass `ownerDocument` when the component renders in an iframe or non-standard document. - `event.preventDefault()` inside the callback can prevent other Escape handlers from firing (since capture happens first). --- ## useId **Import:** `@loke/ui/use-id` **File:** `src/hooks/use-id/id.tsx` ### Signature ```tsx function useId(deterministicId?: string): string ``` ### Behavior | React version | ID format | Mechanism | |---|---|---| | 18+ | `"loke-:r0:"` | Delegates to `React.useId` | | 16/17 | `"loke-0"` | Module-level counter, set in `useLayoutEffect` | | SSR (any) | `""` (empty) until hydration | Counter not incremented server-side | When `deterministicId` is provided, it is returned directly with no internal ID generated. ### Notes - IDs are stable across re-renders in all React versions. - On React 16/17, IDs are `""` during SSR and the first render, then populated after `useLayoutEffect`. Guard `aria-*` attributes that require a non-empty ID. - Prefix `"loke-"` is always prepended to generated IDs to namespace them from user-defined IDs. --- ## useSize **Import:** `@loke/ui/use-size` **File:** `src/hooks/use-size/size.tsx` ### Signature ```tsx function useSize( element: HTMLElement | null, ): { width: number; height: number } | undefined ``` ### Behavior - Returns `undefined` before the element mounts (no element reference yet). - Returns `{ width, height }` immediately on mount using `offsetWidth`/`offsetHeight` (synchronous baseline). - Creates a `ResizeObserver` with `box: "border-box"`. Subsequent updates use `borderBoxSize` when available, falling back to `offsetWidth`/`offsetHeight`. - When `element` becomes `null`, returns `undefined`. - Observer is cleaned up (`unobserve`) when `element` changes or the component unmounts. ### Notes - Uses SSR-safe `useLayoutEffect` internally, so no server-side errors. - Popper uses this for arrow size measurement to account for arrow height in `sideOffset`. - Suitable for measuring content that needs to adapt to container dimensions. --- ## usePrevious **Import:** `@loke/ui/use-previous` **File:** `src/hooks/use-previous/previous.tsx` ### Signature ```tsx function usePrevious<T>(value: T): T ``` ### Behavior Returns the value from the previous render. On the first render, returns the initial value (there is no prior render value). Updates only when `value` changes (compared with `Object.is`). Implemented via `useRef` + `useMemo` to avoid an extra render cycle that `useState`-based implementations would require. ### Notes - The previous value is the value from the render before the current one, not from a previous state update within the same render cycle. - Use for detecting direction of change (e.g., previous tab index to determine slide direction). --- ## useLayoutEffect (SSR-safe) **Import:** `@loke/ui/use-layout-effect` **File:** `src/hooks/use-layout-effect/layout-effect.tsx` ### Signature ```tsx const useLayoutEffect: typeof React.useLayoutEffect ``` ### Behavior - On the client (`globalThis.document` is defined): identical to `React.useLayoutEffect`. Runs synchronously after DOM mutations, before paint. - On the server: no-op function. No warning emitted. ### When to use vs useIsHydrated | Need | Use | |---|---| | DOM measurement/mutation after render | `useLayoutEffect` | | Preventing render of browser-only JSX (portals, `document.*`) | `useIsHydrated` | | Delaying a side effect until client | `useLayoutEffect` | | Showing different markup server vs client | `useIsHydrated` | --- ## useIsHydrated **Import:** `@loke/ui/use-is-hydrated` **File:** `src/hooks/use-is-hydrated/is-hydrated.ts` ### Signature ```tsx function useIsHydrated(): boolean ``` ### Behavior Uses `useSyncExternalStore` with: - Server snapshot: `() => false` - Client snapshot: `() => true` - Subscribe: no-op (state never changes after hydration) Returns `false` during SSR and on the initial client render before hydration completes. Returns `true` on all subsequent renders. ### Notes - Safe to use in components rendered on the server — no hydration mismatch because `useSyncExternalStore` coordinates the server/client transition. - The hook's state never changes after the first `true` — no re-subscription or re-render from the external store. - Do NOT use to gate `useEffect` or `useLayoutEffect` — those hooks are already client-only. Use `useLayoutEffect` from `@loke/ui/use-layout-effect` instead. --- ## useIsDocumentHidden **Import:** `@loke/ui/use-is-document-hidden` **File:** `src/hooks/use-is-document-hidden/is-document-hidden.ts` ### Signature ```tsx function useIsDocumentHidden(): boolean ``` ### Behavior Returns `document.hidden` and subscribes to `visibilitychange` events. Updates state whenever the user switches tabs, minimizes the browser, or the OS hides the window. ### Notes - Initializes with `document.hidden` directly — will throw on SSR if called without guarding. Wrap with `useIsHydrated` check or render only on the client. - Useful for pausing timers, animations, or polling when the tab is not visible. - Tooltip uses this (via `useIsDocumentHidden`) to close open tooltips when the document is hidden, preventing tooltips from remaining visible when the user returns to the tab.