@loke/ui
Version:
292 lines (183 loc) • 9.28 kB
Markdown
# 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.