{"id":28392579,"url":"https://github.com/prakhardubey2002/preact-missing-hooks","last_synced_at":"2026-03-06T18:09:22.394Z","repository":{"id":296006993,"uuid":"990467221","full_name":"prakhardubey2002/Preact-Missing-Hooks","owner":"prakhardubey2002","description":"A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.","archived":false,"fork":false,"pushed_at":"2025-05-28T15:04:12.000Z","size":165,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-07T12:48:18.389Z","etag":null,"topics":["hooks","microbundle","preact","preact-hooks","react-hooks","typescript","usetransition"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/preact-missing-hooks","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/prakhardubey2002.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-26T06:51:55.000Z","updated_at":"2025-05-28T15:00:56.000Z","dependencies_parsed_at":"2025-05-28T14:46:16.369Z","dependency_job_id":"e9b9822f-defe-4aa8-a55d-9b8b4a1a1654","html_url":"https://github.com/prakhardubey2002/Preact-Missing-Hooks","commit_stats":null,"previous_names":["prakhardubey2002/preact-missing-hooks"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/prakhardubey2002/Preact-Missing-Hooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prakhardubey2002%2FPreact-Missing-Hooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prakhardubey2002%2FPreact-Missing-Hooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prakhardubey2002%2FPreact-Missing-Hooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prakhardubey2002%2FPreact-Missing-Hooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/prakhardubey2002","download_url":"https://codeload.github.com/prakhardubey2002/Preact-Missing-Hooks/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prakhardubey2002%2FPreact-Missing-Hooks/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262001346,"owners_count":23243051,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["hooks","microbundle","preact","preact-hooks","react-hooks","typescript","usetransition"],"created_at":"2025-05-31T14:00:20.566Z","updated_at":"2026-03-06T18:09:22.384Z","avatar_url":"https://github.com/prakhardubey2002.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Preact Missing Hooks\n\n\u003cp align=\"left\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/preact-missing-hooks\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/v/preact-missing-hooks?color=crimson\u0026label=npm%20version\" alt=\"npm version\" /\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/preact-missing-hooks\"\u003e\n    \u003cimg src=\"https://img.shields.io/npm/dt/preact-missing-hooks?label=total%20downloads\" alt=\"total downloads\" /\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://github.com/prakhardubey2002/preact-missing-hooks/actions/workflows/test-hooks.yml\"\u003e\n    \u003cimg src=\"https://img.shields.io/github/actions/workflow/status/prakhardubey2002/preact-missing-hooks/test-hooks.yml?branch=main\u0026label=build%20status\" alt=\"Build Status\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nIf this package helps you, please consider dropping a star on the [GitHub repo](https://github.com/prakhardubey2002/Preact-Missing-Hooks).\n\nA lightweight, extendable collection of React-like hooks for Preact, including utilities for transitions, DOM mutation observation, global event buses, theme detection, network status, clipboard access, rage-click detection (e.g. for Sentry), a priority task queue (sequential or parallel), a production-ready **IndexedDB** hook with tables, transactions, and a full CRUD API, and **WebRTC-based IP detection** (`useWebRTCIP`) for frontend-only IP hints.\n\n---\n\n## Features\n\n- **`useTransition`** — Defers state updates to yield a smoother UI experience.\n- **`useMutationObserver`** — Reactively observes DOM changes with a familiar hook API.\n- **`useEventBus`** — A simple publish/subscribe system, eliminating props drilling or overuse of context.\n- **`useWrappedChildren`** — Injects props into child components with flexible merging strategies.\n- **`usePreferredTheme`** — Detects the user's preferred color scheme (light/dark) from system preferences.\n- **`useNetworkState`** — Tracks online/offline status and connection details (type, downlink, RTT, save-data).\n- **`usePrefetch`** — Preload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading. Returns `prefetch(url, options?)` and `isPrefetched(url)`.\n- **`useClipboard`** — Copy and paste text with the Clipboard API, with copied/error state.\n- **`useRageClick`** — Detects rage clicks (repeated rapid clicks in the same spot). Use with Sentry or similar to detect and fix rage-click issues and lower rage-click-related support.\n- **`useThreadedWorker`** — Run async work in a queue with **sequential** (single worker, priority-ordered) or **parallel** (worker pool) mode. Optional priority (1 = highest); FIFO within same priority.\n- **`useIndexedDB`** — IndexedDB abstraction with database/table init, insert, update, delete, exists, query (cursor + filter), upsert, bulk insert, clear, count, and full transaction support. Singleton connection, Promise-based API, optional `onSuccess`/`onError` callbacks.\n- **`useWebRTCIP`** — Detects client IP addresses using WebRTC ICE candidates and a STUN server (frontend-only). **Not highly reliable**; use as a first-priority hint and fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)) when it fails or returns empty.\n- **`useWasmCompute`** — Runs WebAssembly computation off the main thread via a Web Worker. Validates environment (browser, Worker, WebAssembly) and returns `compute(input)`, `result`, `loading`, `error`, `ready`.\n- **`useWorkerNotifications`** — Listens to a Worker's messages and maintains state: running tasks, completed/failed counts, event history, average task duration, throughput per second, and queue size. Worker posts `task_start` / `task_end` / `task_fail` / `queue_size`; returns `progress` (default view of all active worker data) plus individual stats.\n- **`useLLMMetadata`** — Injects an AI-readable metadata block into the document head on route change. Works in React 18+ and Preact 10+. Supports **manual** (title, description, tags) and **auto-extract** (from `document.title`, visible `h1`/`h2`, first 3 `p`). Cacheable, SSR-safe, no router dependency.\n- **`useRefPrint`** — Binds a ref to a printable section and provides `print()` to open the native print dialog. Uses `@media print` CSS so only that section is printed (or saved as PDF). Options: `documentTitle`, `downloadAsPdf`.\n- **`useRBAC`** — Frontend-only role-based access control. Define roles with conditions, assign capabilities per role. Pluggable user source: `localStorage`, `sessionStorage`, API, memory, or custom. Returns `user`, `roles`, `capabilities`, `hasRole(role)`, `can(capability)`, and storage helpers.\n- Fully TypeScript compatible\n- Bundled with Microbundle\n- Zero dependencies (peer: `preact` or `react` — use `/react` for React)\n\n---\n\n## Installation\n\n```bash\nnpm install preact-missing-hooks\n```\n\nEnsure your app has either **preact** or **react** installed (the package uses whichever is present).\n\n---\n\n## Import options\n\nUse the same import in Preact and React projects:\n\n```ts\nimport { useThreadedWorker, useClipboard } from \"preact-missing-hooks\";\n```\n\n- **How it picks Preact vs React**\n  - **CommonJS / Node:** The package detects which of `preact` or `react` is installed and uses that build automatically.\n  - **ESM (Vite, Webpack, etc.):** Default is the Preact build. In a **React** app, add the `react` condition so the package resolves to the React build:\n    - **Vite:** `vite.config.ts` → `resolve: { conditions: ['react'] }`\n    - **Webpack:** `resolve.conditionNames` (or similar) to include `'react'`\n  - **Or** in React projects you can always import from the explicit entry: `preact-missing-hooks/react`.\n\n- **Subpath exports (tree-shakeable)** — Import a single hook:\n\n  ```ts\n  import { useThreadedWorker } from \"preact-missing-hooks/useThreadedWorker\";\n  import { useClipboard } from \"preact-missing-hooks/useClipboard\";\n  import { usePrefetch } from \"preact-missing-hooks/usePrefetch\";\n  import { useWebRTCIP } from \"preact-missing-hooks/useWebRTCIP\";\n  import { useWasmCompute } from \"preact-missing-hooks/useWasmCompute\";\n  import { useWorkerNotifications } from \"preact-missing-hooks/useWorkerNotifications\";\n  ```\n\n  All hooks are available: `useTransition`, `useMutationObserver`, `useEventBus`, `useWrappedChildren`, `usePreferredTheme`, `useNetworkState`, `useClipboard`, `usePrefetch`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`, `useLLMMetadata`, `useRefPrint`, `useRBAC`.\n\n---\n\n## Quick start\n\nMinimal example (Preact or React):\n\n```tsx\nimport {\n  useTransition,\n  useClipboard,\n  usePreferredTheme,\n} from \"preact-missing-hooks\";\n\nfunction App() {\n  const [startTransition, isPending] = useTransition();\n  const { copy, copied } = useClipboard();\n  const theme = usePreferredTheme();\n\n  return (\n    \u003cdiv\u003e\n      \u003cbutton\n        onClick={() =\u003e\n          startTransition(() =\u003e {\n            /* heavy update */\n          })\n        }\n        disabled={isPending}\n      \u003e\n        {isPending ? \"Loading…\" : \"Update\"}\n      \u003c/button\u003e\n      \u003cbutton onClick={() =\u003e copy(\"Hello!\")}\u003e\n        {copied ? \"Copied!\" : \"Copy\"}\n      \u003c/button\u003e\n      \u003cspan\u003eTheme: {theme}\u003c/span\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n**Live demo:** Try every hook with live examples:\n\n- **Online:** [preact-missing-hooks.vercel.app](https://preact-missing-hooks.vercel.app/)\n- **Local:** Run the docs demo:\n\n```bash\nnpm run build \u0026\u0026 npx serve -l 5000\n# Open http://localhost:5000/docs/\n```\n\nOr open `docs/index.html` after building (see [docs/README.md](docs/README.md) for details).\n\n**Usage at a glance:**\n\n| Hook                                              | One-liner                                                                                     |\n| ------------------------------------------------- | --------------------------------------------------------------------------------------------- |\n| [useTransition](#usetransition)                   | `const [startTransition, isPending] = useTransition();`                                       |\n| [useMutationObserver](#usemutationobserver)       | `useMutationObserver(ref, callback, { childList: true });`                                    |\n| [useEventBus](#useeventbus)                       | `const { emit, on } = useEventBus();`                                                         |\n| [useWrappedChildren](#usewrappedchildren)         | `const wrapped = useWrappedChildren(children, { className: 'x' });`                           |\n| [usePreferredTheme](#usepreferredtheme)           | `const theme = usePreferredTheme(); // 'light' \\| 'dark' \\| 'no-preference'`                  |\n| [useNetworkState](#usenetworkstate)               | `const { online, effectiveType } = useNetworkState();`                                        |\n| [usePrefetch](#useprefetch)                       | `const { prefetch, isPrefetched } = usePrefetch();`                                           |\n| [useClipboard](#useclipboard)                     | `const { copy, paste, copied } = useClipboard();`                                             |\n| [useRageClick](#userageclick)                     | `useRageClick(ref, { onRageClick, threshold: 5 });`                                           |\n| [useThreadedWorker](#usethreadedworker)           | `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });`             |\n| [useIndexedDB](#useindexeddb)                     | `const { db, isReady } = useIndexedDB({ name, version, tables });`                            |\n| [useWebRTCIP](#usewebrtcip)                       | `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });`                             |\n| [useWasmCompute](#usewasmcompute)                 | `const { compute, result, ready } = useWasmCompute({ wasmUrl });`                             |\n| [useWorkerNotifications](#useworkernotifications) | `const { progress, eventHistory } = useWorkerNotifications(worker);`                          |\n| [useLLMMetadata](#usellmmetadata)                 | `useLLMMetadata({ route: pathname, mode: 'auto-extract' });`                                  |\n| [useRefPrint](#userefprint)                       | `const { print } = useRefPrint(printRef, { documentTitle: 'Report' });`                       |\n| [useRBAC](#userbac)                               | `const { can, hasRole, roles } = useRBAC({ userSource, roleDefinitions, roleCapabilities });` |\n\n---\n\n## Usage Examples\n\n### `useTransition`\n\n```tsx\nimport { useTransition } from \"preact-missing-hooks\";\n\nfunction ExampleTransition() {\n  const [startTransition, isPending] = useTransition();\n\n  const handleClick = () =\u003e {\n    startTransition(() =\u003e {\n      // perform an expensive update here\n    });\n  };\n\n  return (\n    \u003cbutton onClick={handleClick} disabled={isPending}\u003e\n      {isPending ? \"Loading...\" : \"Click Me\"}\n    \u003c/button\u003e\n  );\n}\n```\n\n---\n\n### `useMutationObserver`\n\n```tsx\nimport { useRef } from \"preact/hooks\";\nimport { useMutationObserver } from \"preact-missing-hooks\";\n\nfunction ExampleMutation() {\n  const ref = useRef\u003cHTMLDivElement\u003e(null);\n\n  useMutationObserver(\n    ref,\n    (mutations) =\u003e {\n      console.log(\"Detected mutations:\", mutations);\n    },\n    { childList: true, subtree: true }\n  );\n\n  return \u003cdiv ref={ref}\u003eObserve this content\u003c/div\u003e;\n}\n```\n\n---\n\n### `useEventBus`\n\n```tsx\n// types.ts\nexport type Events = {\n  notify: (message: string) =\u003e void;\n};\n\n// Sender.tsx\nimport { useEventBus } from \"preact-missing-hooks\";\nimport type { Events } from \"./types\";\n\nfunction Sender() {\n  const { emit } = useEventBus\u003cEvents\u003e();\n  return \u003cbutton onClick={() =\u003e emit(\"notify\", \"Hello World!\")}\u003eSend\u003c/button\u003e;\n}\n\n// Receiver.tsx\nimport { useEventBus } from \"preact-missing-hooks\";\nimport { useState, useEffect } from \"preact/hooks\";\nimport type { Events } from \"./types\";\n\nfunction Receiver() {\n  const [msg, setMsg] = useState\u003cstring\u003e(\"\");\n  const { on } = useEventBus\u003cEvents\u003e();\n\n  useEffect(() =\u003e {\n    const unsubscribe = on(\"notify\", setMsg);\n    return unsubscribe;\n  }, []);\n\n  return \u003cdiv\u003eMessage: {msg}\u003c/div\u003e;\n}\n```\n\n---\n\n### `useWrappedChildren`\n\n```tsx\nimport { useWrappedChildren } from \"preact-missing-hooks\";\n\nfunction ParentComponent({ children }) {\n  // Inject common props into all children\n  const injectProps = {\n    className: \"enhanced-child\",\n    onClick: () =\u003e console.log(\"Child clicked!\"),\n    style: { border: \"1px solid #ccc\" },\n  };\n\n  const wrappedChildren = useWrappedChildren(children, injectProps);\n\n  return \u003cdiv className=\"parent\"\u003e{wrappedChildren}\u003c/div\u003e;\n}\n\n// Usage with preserve strategy (default - existing props are preserved)\nfunction PreserveExample() {\n  return (\n    \u003cParentComponent\u003e\n      \u003cbutton className=\"btn\"\u003eExisting class preserved\u003c/button\u003e\n      \u003cspan style={{ color: \"red\" }}\u003eBoth styles applied\u003c/span\u003e\n    \u003c/ParentComponent\u003e\n  );\n}\n\n// Usage with override strategy (injected props override existing ones)\nfunction OverrideExample() {\n  const injectProps = { className: \"new-class\" };\n  const children = (\n    \u003cbutton className=\"old-class\"\u003eClass will be overridden\u003c/button\u003e\n  );\n\n  const wrappedChildren = useWrappedChildren(children, injectProps, \"override\");\n\n  return \u003cdiv\u003e{wrappedChildren}\u003c/div\u003e;\n}\n```\n\n---\n\n### `usePreferredTheme`\n\n```tsx\nimport { usePreferredTheme } from \"preact-missing-hooks\";\n\nfunction ThemeAwareComponent() {\n  const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'\n\n  return \u003cdiv data-theme={theme}\u003eYour system prefers: {theme}\u003c/div\u003e;\n}\n```\n\n---\n\n### `useNetworkState`\n\n```tsx\nimport { useNetworkState } from \"preact-missing-hooks\";\n\nfunction NetworkStatus() {\n  const { online, effectiveType, saveData } = useNetworkState();\n\n  return (\n    \u003cdiv\u003e\n      Status: {online ? \"Online\" : \"Offline\"}\n      {effectiveType \u0026\u0026 ` (${effectiveType})`}\n      {saveData \u0026\u0026 \" — Reduced data mode enabled\"}\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n### `useClipboard`\n\n```tsx\nimport { useState } from \"preact/hooks\";\nimport { useClipboard } from \"preact-missing-hooks\";\n\nfunction CopyButton() {\n  const { copy, copied, error } = useClipboard({ resetDelay: 2000 });\n\n  return (\n    \u003cbutton onClick={() =\u003e copy(\"Hello, World!\")}\u003e\n      {copied ? \"Copied!\" : \"Copy\"}\n    \u003c/button\u003e\n  );\n}\n\nfunction PasteInput() {\n  const [text, setText] = useState(\"\");\n  const { paste } = useClipboard();\n\n  const handlePaste = async () =\u003e {\n    const content = await paste();\n    setText(content);\n  };\n\n  return (\n    \u003cdiv\u003e\n      \u003cinput value={text} onChange={(e) =\u003e setText(e.target.value)} /\u003e\n      \u003cbutton onClick={handlePaste}\u003ePaste\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n### `usePrefetch`\n\nPreload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading. Use `prefetch(url)` with optional `{ as: 'document' | 'fetch' }`; `as: 'fetch'` warms the HTTP cache (e.g. for API URLs).\n\n```tsx\nimport { usePrefetch } from \"preact-missing-hooks\";\n\nfunction NavLink({ href, children }) {\n  const { prefetch, isPrefetched } = usePrefetch();\n  return (\n    \u003ca href={href} onMouseEnter={() =\u003e prefetch(href)}\u003e\n      {children}\n      {isPrefetched(href) \u0026\u0026 \" ✓\"}\n    \u003c/a\u003e\n  );\n}\n\n// Prefetch API data\nfunction DataLoader() {\n  const { prefetch } = usePrefetch();\n  prefetch(\"/api/user\", { as: \"fetch\" });\n  // ...\n}\n```\n\n---\n\n### `useRageClick`\n\nDetects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.\n\n```tsx\nimport { useRef } from \"preact/hooks\";\nimport { useRageClick } from \"preact-missing-hooks\";\n\nfunction SubmitButton() {\n  const ref = useRef\u003cHTMLButtonElement\u003e(null);\n\n  useRageClick(ref, {\n    onRageClick: ({ count, event }) =\u003e {\n      // Report to Sentry (or your error tracker) to create rage-click issues\n      Sentry.captureMessage(\"Rage click detected\", {\n        level: \"warning\",\n        extra: { count, target: event.target, tag: \"rage_click\" },\n      });\n    },\n    threshold: 5, // min clicks (default 5, Sentry-style)\n    timeWindow: 1000, // ms (default 1000)\n    distanceThreshold: 30, // px (default 30)\n  });\n\n  return \u003cbutton ref={ref}\u003eSubmit\u003c/button\u003e;\n}\n```\n\n---\n\n### `useThreadedWorker`\n\nRuns async work in a queue with **sequential** (one task at a time, by priority) or **parallel** (worker pool) execution. Lower priority number = higher priority; same priority is FIFO.\n\n```tsx\nimport { useThreadedWorker } from \"preact-missing-hooks\";\n\n// Sequential: one task at a time, sorted by priority\nconst sequential = useThreadedWorker(fetchUser, { mode: \"sequential\" });\n\n// Parallel: up to N tasks at once\nconst parallel = useThreadedWorker(processItem, {\n  mode: \"parallel\",\n  concurrency: 4,\n});\n\n// API (same for both modes)\nconst {\n  run, // (data, { priority?: number }) =\u003e Promise\u003cTResult\u003e\n  loading, // true while any task is queued or running\n  result, // last successful result\n  error, // last error\n  queueSize, // tasks queued + running\n  clearQueue, // clear pending tasks (running continue)\n  terminate, // clear queue and reject new run()\n} = sequential;\n\n// Run with priority (1 = highest)\nawait run({ userId: 1 }, { priority: 1 });\nawait run({ userId: 2 }, { priority: 3 });\n```\n\n---\n\n### `useIndexedDB`\n\nProduction-ready IndexedDB hook: database initialization, table creation (with keyPath, autoIncrement, indexes), singleton connection, and a full table API. All operations are Promise-based and support optional `onSuccess`/`onError` callbacks.\n\n**Config:** `name`, `version`, and `tables` (each table: `keyPath`, `autoIncrement?`, `indexes?`).\n\n**Table API:** `insert`, `update`, `delete`, `exists`, `query(filterFn)`, `upsert`, `bulkInsert`, `clear`, `count`.\n\n**Database API:** `db.table(name)`, `db.hasTable(name)`, `db.transaction(storeNames, mode, callback, options?)`.\n\n```tsx\nimport { useIndexedDB } from \"preact-missing-hooks\";\n\nfunction App() {\n  const { db, isReady, error } = useIndexedDB({\n    name: \"my-app-db\",\n    version: 1,\n    tables: {\n      users: { keyPath: \"id\", autoIncrement: true, indexes: [\"email\"] },\n      settings: { keyPath: \"key\" },\n    },\n  });\n\n  if (error) return \u003cdiv\u003eFailed to open database\u003c/div\u003e;\n  if (!isReady || !db) return \u003cdiv\u003eLoading...\u003c/div\u003e;\n\n  const users = db.table(\"users\");\n\n  // All operations return Promises and accept optional { onSuccess, onError }\n  await users.insert({ email: \"a@b.com\", name: \"Alice\" });\n  await users.update(1, { name: \"Alice Smith\" });\n  const found = await users.query((u) =\u003e u.email.startsWith(\"a@\"));\n  const n = await users.count();\n  await users.delete(1);\n  await users.upsert({ id: 2, email: \"b@b.com\" });\n  await users.bulkInsert([{ email: \"c@b.com\" }, { email: \"d@b.com\" }]);\n  await users.clear();\n\n  // Full transaction support\n  await db.transaction([\"users\", \"settings\"], \"readwrite\", async (tx) =\u003e {\n    await tx.table(\"users\").insert({ email: \"e@b.com\" });\n    await tx.table(\"settings\").upsert({ key: \"theme\", value: \"dark\" });\n  });\n\n  return \u003cdiv\u003eDB ready. Tables: {db.hasTable(\"users\") ? \"users\" : \"\"}\u003c/div\u003e;\n}\n```\n\n---\n\n### `useWebRTCIP`\n\nDetects client IP addresses using WebRTC ICE candidates and a STUN server (**frontend-only**, no backend). **Not highly reliable** — use as a **first-priority** hint; if it fails or returns empty, fall back to a public IP API (e.g. [ipapi.co](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)).\n\nReturns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.\n\n```tsx\nimport { useWebRTCIP } from \"preact-missing-hooks\";\nimport { useState, useEffect } from \"preact/hooks\";\n\nfunction ClientIP() {\n  const { ips, loading, error } = useWebRTCIP({\n    timeout: 4000,\n    onDetect: (ip) =\u003e {\n      /* optional: e.g. analytics */\n    },\n  });\n  const [fallbackIP, setFallbackIP] = useState\u003cstring | null\u003e(null);\n\n  // Fallback to public IP API when WebRTC fails or returns empty\n  useEffect(() =\u003e {\n    if (loading || ips.length \u003e 0) return;\n    if (error) {\n      fetch(\"https://api.ipify.org?format=json\")\n        .then((r) =\u003e r.json())\n        .then((d) =\u003e setFallbackIP(d.ip))\n        .catch(() =\u003e {});\n    }\n  }, [loading, ips.length, error]);\n\n  if (loading) return \u003cp\u003eDetecting IP…\u003c/p\u003e;\n  if (ips.length \u003e 0) return \u003cp\u003eIPs (WebRTC): {ips.join(\", \")}\u003c/p\u003e;\n  if (fallbackIP) return \u003cp\u003eIP (fallback API): {fallbackIP}\u003c/p\u003e;\n  if (error) return \u003cp\u003eWebRTC failed. Try fallback API.\u003c/p\u003e;\n  return null;\n}\n```\n\n---\n\n### `useWasmCompute`\n\nRuns WebAssembly computation in a Web Worker so the main thread stays responsive. Flow: **Preact Component → useWasmCompute() → Web Worker → WASM Module → return result.** The hook checks that the environment supports `window`, `Worker`, and `WebAssembly`; in SSR or unsupported environments it sets `error` and leaves `ready` false.\n\nReturns `{ compute, result, loading, error, ready }`. Options: `wasmUrl` (required), `exportName` (default `'compute'`), optional `workerUrl` (custom worker script), optional `importObject` (must be serializable for the default worker).\n\n```tsx\nimport { useWasmCompute } from \"preact-missing-hooks\";\n\nfunction AddWithWasm() {\n  const { compute, result, loading, error, ready } = useWasmCompute\u003c\n    number,\n    number\n  \u003e({\n    wasmUrl: \"/add.wasm\",\n    exportName: \"add\",\n  });\n\n  const handleClick = () =\u003e {\n    if (ready) compute(2).then(() =\u003e {});\n  };\n\n  if (error) return \u003cp\u003eWASM unavailable: {error}\u003c/p\u003e;\n  if (!ready) return \u003cp\u003eLoading WASM…\u003c/p\u003e;\n  return (\n    \u003cdiv\u003e\n      \u003cbutton onClick={handleClick} disabled={loading}\u003e\n        Add 2\n      \u003c/button\u003e\n      {result != null \u0026\u0026 \u003cp\u003eResult: {result}\u003c/p\u003e}\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n### `useWorkerNotifications`\n\nListens to a Worker's `message` events and maintains state and derived stats. Your worker should `postMessage` with: `{ type: 'task_start', taskId? }`, `{ type: 'task_end', taskId?, duration? }`, `{ type: 'task_fail', taskId?, error? }`, and optionally `{ type: 'queue_size', size }`.\n\nReturns `runningTasks`, `completedCount`, `failedCount`, `eventHistory`, `averageDurationMs`, `throughputPerSecond`, `currentQueueSize`, and **`progress`** — a single object with all active worker data (running, completed, failed, totalProcessed, avg duration, throughput/s, queue). Options: `maxHistory` (default 100), `throughputWindowMs` (default 1000).\n\n```tsx\nimport { useWorkerNotifications } from \"preact-missing-hooks\";\n\nfunction WorkerDashboard({ worker }) {\n  const { progress, eventHistory } = useWorkerNotifications(worker, {\n    maxHistory: 50,\n  });\n\n  return (\n    \u003cdiv\u003e\n      \u003cp\u003e\n        Running: {progress.runningTasks.length} | Done:{\" \"}\n        {progress.completedCount} | Failed: {progress.failedCount}\n      \u003c/p\u003e\n      \u003cp\u003e\n        Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{\" \"}\n        {progress.throughputPerSecond.toFixed(2)}/s | Queue:{\" \"}\n        {progress.currentQueueSize}\n      \u003c/p\u003e\n      \u003csmall\u003eEvents: {eventHistory.length}\u003c/small\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n### `useLLMMetadata`\n\nInjects an AI-readable metadata block into the document head when the route changes. Works in **React 18+** and **Preact 10+** (framework-agnostic). No router dependency — you pass the current `route` string and the hook updates the script when it changes.\n\n**Safe usage:** The hook **never throws**. It accepts `config` or `null`/`undefined`. When `config` is `null` or `undefined`, it injects a minimal payload with `route: \"/\"` and `generatedAt`. Invalid or missing values are normalized; all strings are length-limited and URLs validated; DOM access is wrapped in try/catch. Safe for SSR (no-op when `window` is undefined).\n\n**API:**\n\n```ts\ntype OGType =\n  | \"website\"\n  | \"article\"\n  | \"profile\"\n  | \"video.other\"\n  | \"product\"\n  | \"music.song\"\n  | \"book\";\n\ninterface LLMConfig {\n  route: string;\n  mode?: \"manual\" | \"auto-extract\";\n  title?: string;\n  description?: string;\n  tags?: string[];\n  canonicalUrl?: string; // absolute URL\n  language?: string; // e.g. \"en\", \"en-US\"\n  ogType?: OGType; // Open Graph type\n  ogImage?: string; // absolute image URL\n  ogImageAlt?: string;\n  siteName?: string;\n  author?: string;\n  publishedTime?: string; // ISO date\n  modifiedTime?: string; // ISO date\n  robots?: string; // e.g. \"index, follow\"\n  extra?: Record\u003cstring, string | number | boolean | string[]\u003e;\n}\n\nfunction useLLMMetadata(config: LLMConfig | null | undefined): void;\n```\n\n**Behavior:**\n\n- When `config` is `null` or `undefined`: injects a minimal payload with `route: \"/\"` and `generatedAt` (no throw).\n- When `config.route` (or other deps) change: removes any existing `\u003cscript data-llm=\"true\"\u003e`, then injects a new one.\n- Script tag: `\u003cscript type=\"application/llm+json\" data-llm=\"true\"\u003e` with JSON payload. Only defined, safe fields are included.\n- **Cacheable:** If the generated payload is unchanged, the script is not replaced.\n- **SSR-safe:** No-op when `typeof window === \"undefined\"`.\n- Cleans up on unmount (removes the script).\n\n**Modes:**\n\n- **`manual`** (default): Uses `title`, `description`, `tags`, and any other config fields you pass.\n- **`auto-extract`**: Fills `title`, `description`, and `outline` from the DOM (`document.title`, visible `\u003ch1\u003e`/`\u003ch2\u003e`, first 3 visible `\u003cp\u003e`). You can still override with config. Ignores content inside `nav`, `footer`, `script`, `style`.\n\n**Example payload (rich):**\n\n```json\n{\n  \"route\": \"/blog/ai-hooks\",\n  \"title\": \"AI Hooks in Preact\",\n  \"description\": \"A short summary...\",\n  \"tags\": [\"preact\", \"react\", \"hooks\"],\n  \"outline\": [\"Intro\", \"Problem\", \"Solution\"],\n  \"canonicalUrl\": \"https://example.com/blog/ai-hooks\",\n  \"language\": \"en\",\n  \"ogType\": \"article\",\n  \"ogImage\": \"https://example.com/og.png\",\n  \"siteName\": \"My Blog\",\n  \"author\": \"Jane Doe\",\n  \"publishedTime\": \"2025-02-14T10:00:00.000Z\",\n  \"modifiedTime\": \"2025-02-14T12:00:00.000Z\",\n  \"robots\": \"index, follow\",\n  \"generatedAt\": \"2025-02-14T12:00:00.000Z\"\n}\n```\n\n**Example: React Router**\n\n```tsx\nimport { useLocation } from \"react-router-dom\";\nimport { useLLMMetadata } from \"preact-missing-hooks\"; // or \"preact-missing-hooks/react\"\n\nfunction App() {\n  const { pathname } = useLocation();\n  useLLMMetadata({\n    route: pathname,\n    mode: \"auto-extract\",\n    title: document.title,\n    tags: [\"my-app\"],\n  });\n  return \u003cOutlet /\u003e;\n}\n```\n\n**Example: Preact Router**\n\n```tsx\nimport { useLocation } from \"preact-router\";\nimport { useLLMMetadata } from \"preact-missing-hooks\";\n\nfunction App() {\n  const [pathname] = useLocation();\n  useLLMMetadata({\n    route: pathname ?? \"/\",\n    mode: \"manual\",\n    title: \"My Page\",\n    description: \"Page description\",\n    tags: [\"preact\", \"hooks\"],\n  });\n  return \u003cdiv\u003e{/* your routes / children */}\u003c/div\u003e;\n}\n```\n\n---\n\n### `useRefPrint`\n\nBinds a ref to a DOM section and provides `print()` to open the native print dialog. Uses `@media print` CSS so only that section is visible when printing (user can then print or choose “Save as PDF”). Options: `documentTitle` (title for the print document), `downloadAsPdf` (hint that the same flow supports saving as PDF).\n\n```tsx\nimport { useRef } from \"preact/hooks\";\nimport { useRefPrint } from \"preact-missing-hooks\";\n\nfunction Report() {\n  const printRef = useRef\u003cHTMLDivElement\u003e(null);\n  const { print } = useRefPrint(printRef, {\n    documentTitle: \"Monthly Report\",\n    downloadAsPdf: true,\n  });\n\n  return (\n    \u003cdiv\u003e\n      \u003cdiv ref={printRef}\u003e\n        \u003ch1\u003eReport content\u003c/h1\u003e\n        \u003cp\u003eOnly this section is printed when you click Print.\u003c/p\u003e\n      \u003c/div\u003e\n      \u003cbutton onClick={print}\u003ePrint / Save as PDF\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n### `useRBAC`\n\nFrontend-only role-based access control. Define roles with a condition (e.g. `user.role === 'admin'`), assign capabilities per role (use `'*'` for full access), and plug in where the current user comes from: `localStorage`, `sessionStorage`, API, memory, or a custom getter. Returns `user`, `roles`, `capabilities`, `hasRole(role)`, `can(capability)`, `refetch`, and helpers like `setUserInStorage` for persisting auth in storage.\n\n**User source types:** `localStorage`, `sessionStorage` (key to read user JSON), `api` (`fetch` returning user), `memory` (`getUser()`), `custom` (`getAuth()` returning `{ user?, roles?, capabilities? }`). Optional `capabilitiesOverride` can read capabilities from storage or API instead of deriving from roles.\n\n```tsx\nimport { useRBAC } from \"preact-missing-hooks\";\n\nconst roleDefinitions = [\n  { role: \"admin\", condition: (u) =\u003e u?.role === \"admin\" },\n  {\n    role: \"editor\",\n    condition: (u) =\u003e u?.role === \"editor\" || u?.role === \"admin\",\n  },\n  { role: \"viewer\", condition: (u) =\u003e !!u?.id },\n];\nconst roleCapabilities = {\n  admin: [\"*\"],\n  editor: [\"posts:edit\", \"posts:create\", \"posts:read\"],\n  viewer: [\"posts:read\"],\n};\n\nfunction App() {\n  const { user, roles, capabilities, hasRole, can, setUserInStorage } = useRBAC(\n    {\n      userSource: { type: \"localStorage\", key: \"user\" },\n      roleDefinitions,\n      roleCapabilities,\n    }\n  );\n\n  const login = (role) =\u003e {\n    setUserInStorage(\n      { id: 1, role, email: role + \"@app.com\" },\n      \"localStorage\",\n      \"user\"\n    );\n  };\n  const logout = () =\u003e setUserInStorage(null, \"localStorage\", \"user\");\n\n  return (\n    \u003cdiv\u003e\n      {!user ? (\n        \u003cdiv\u003e\n          \u003cbutton onClick={() =\u003e login(\"admin\")}\u003eLogin as Admin\u003c/button\u003e\n          \u003cbutton onClick={() =\u003e login(\"editor\")}\u003eLogin as Editor\u003c/button\u003e\n          \u003cbutton onClick={() =\u003e login(\"viewer\")}\u003eLogin as Viewer\u003c/button\u003e\n        \u003c/div\u003e\n      ) : (\n        \u003cdiv\u003e\n          \u003cp\u003eRoles: {roles.join(\", \")}\u003c/p\u003e\n          {can(\"posts:edit\") \u0026\u0026 \u003cbutton\u003eEdit post\u003c/button\u003e}\n          {can(\"*\") \u0026\u0026 \u003cbutton\u003eAdmin panel\u003c/button\u003e}\n          \u003cbutton onClick={logout}\u003eLogout\u003c/button\u003e\n        \u003c/div\u003e\n      )}\n    \u003c/div\u003e\n  );\n}\n```\n\n---\n\n## Built With\n\n- [Preact](https://preactjs.com)\n- [Microbundle](https://github.com/developit/microbundle)\n- [TypeScript](https://www.typescriptlang.org)\n- [Vitest](https://vitest.dev) for testing\n\n---\n\n## License\n\nMIT © [Prakhar Dubey](https://github.com/prakhardubey2002)\n\n---\n\n## Contributing\n\nContributions are welcome! Please open issues or submit PRs with new hooks or improvements.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprakhardubey2002%2Fpreact-missing-hooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprakhardubey2002%2Fpreact-missing-hooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprakhardubey2002%2Fpreact-missing-hooks/lists"}