Skip to content

Repository files navigation

Preact Missing Hooks

npm version total downloads Build Status

If this package helps you, please consider dropping a star on the GitHub repo.

A 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, debounce, idle detection, click-outside, media queries, 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.


Features

  • useTransition — Defers state updates to yield a smoother UI experience.
  • useMutationObserver — Reactively observes DOM changes with a familiar hook API.
  • useEventBus — A simple publish/subscribe system, eliminating props drilling or overuse of context.
  • useWrappedChildren — Injects props into child components with flexible merging strategies.
  • usePreferredTheme — Detects the user's preferred color scheme (light/dark) from system preferences.
  • useNetworkState — Tracks online/offline status and connection details (type, downlink, RTT, save-data).
  • 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).
  • usePoll — Polls an async function at a fixed interval until it returns { done: true, data? }. Stops on error. Returns data, done, error, pollCount, start, stop. Good for readiness checks or waiting on a backend job.
  • useDeviceData — Extracts device and browser data from native Navigator, Screen, window, and matchMedia APIs (browser name/version, OS name/version, language, platform, CPUs, memory, screen/viewport size, touch, color scheme, reduced motion, Client Hints). Uses Client Hints high-entropy values when available. Optionally polls the Battery Status API. Updates on resize, orientation, and preference changes.
  • useDebounce — Delays updating a value or executing a function until a specified time has passed without further changes. Returns the debounced value, or a debounced function with cancel() / flush().
  • useIdle — Detects when a user has been inactive for a specified period of time. Returns idle, lastActive, and reset().
  • useClickOutside — Detects clicks outside a specified element, useful for closing dropdowns, modals, and popovers.
  • useMediaQuery — Tracks whether a CSS media query currently matches the browser environment.
  • useClipboard — Copy and paste text with the Clipboard API, with copied/error state.
  • 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.
  • 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.
  • 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.
  • 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, ipify, ip-api.com) when it fails or returns empty.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • Fully TypeScript compatible
  • Bundled with Microbundle
  • Zero dependencies (peer: preact or react — use /react for React)

Installation

npm install preact-missing-hooks

Ensure your app has either preact or react installed (the package uses whichever is present).


Import options

Use the same import in Preact and React projects:

import { useThreadedWorker, useClipboard } from "preact-missing-hooks";
  • How it picks Preact vs React

    • CommonJS / Node: The package detects which of preact or react is installed and uses that build automatically.
    • 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:
      • Vite: vite.config.tsresolve: { conditions: ['react'] }
      • Webpack: resolve.conditionNames (or similar) to include 'react'
    • Or in React projects you can always import from the explicit entry: preact-missing-hooks/react.
  • Subpath exports (tree-shakeable) — Import a single hook:

    import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
    import { useClipboard } from "preact-missing-hooks/useClipboard";
    import { usePrefetch } from "preact-missing-hooks/usePrefetch";
    import { usePoll } from "preact-missing-hooks/usePoll";
    import { useDeviceData } from "preact-missing-hooks/useDeviceData";
    import { useDebounce } from "preact-missing-hooks/useDebounce";
    import { useIdle } from "preact-missing-hooks/useIdle";
    import { useClickOutside } from "preact-missing-hooks/useClickOutside";
    import { useMediaQuery } from "preact-missing-hooks/useMediaQuery";
    import { useWebRTCIP } from "preact-missing-hooks/useWebRTCIP";
    import { useWasmCompute } from "preact-missing-hooks/useWasmCompute";
    import { useWorkerNotifications } from "preact-missing-hooks/useWorkerNotifications";

    All hooks are available: useTransition, useMutationObserver, useEventBus, useWrappedChildren, usePreferredTheme, useNetworkState, useClipboard, usePrefetch, usePoll, useDeviceData, useDebounce, useIdle, useClickOutside, useMediaQuery, useRageClick, useThreadedWorker, useIndexedDB, useWebRTCIP, useWasmCompute, useWorkerNotifications, useLLMMetadata, useRefPrint, useRBAC.


Quick start

Minimal example (Preact or React):

import {
  useTransition,
  useClipboard,
  usePreferredTheme,
} from "preact-missing-hooks";

function App() {
  const [startTransition, isPending] = useTransition();
  const { copy, copied } = useClipboard();
  const theme = usePreferredTheme();

  return (
    <div>
      <button
        onClick={() =>
          startTransition(() => {
            /* heavy update */
          })
        }
        disabled={isPending}
      >
        {isPending ? "Loading…" : "Update"}
      </button>
      <button onClick={() => copy("Hello!")}>
        {copied ? "Copied!" : "Copy"}
      </button>
      <span>Theme: {theme}</span>
    </div>
  );
}

Live demo: Try every hook with live examples:

npm run build && npx serve -l 5000
# Open http://localhost:5000/docs/

Or open docs/index.html after building (see docs/README.md for details).

Usage at a glance:

Hook One-liner
useTransition const [startTransition, isPending] = useTransition();
useMutationObserver useMutationObserver(ref, callback, { childList: true });
useEventBus const { emit, on } = useEventBus();
useWrappedChildren const wrapped = useWrappedChildren(children, { className: 'x' });
usePreferredTheme const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'
useNetworkState const { online, effectiveType } = useNetworkState();
usePrefetch const { prefetch, isPrefetched } = usePrefetch();
usePoll const { data, done, pollCount, stop } = usePoll(pollFn, { intervalMs });
useDeviceData const { browser, os } = useDeviceData();browser.name, os.version, viewport, etc.
useDebounce const debounced = useDebounce(value, 300); / const save = useDebounce(fn, 300);
useIdle const { idle, lastActive, reset } = useIdle(5000);
useClickOutside useClickOutside(ref, () => setOpen(false));
useMediaQuery const isMobile = useMediaQuery("(max-width: 768px)");
useClipboard const { copy, paste, copied } = useClipboard();
useRageClick useRageClick(ref, { onRageClick, threshold: 5 });
useThreadedWorker const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });
useIndexedDB const { db, isReady } = useIndexedDB({ name, version, tables });
useWebRTCIP const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });
useWasmCompute const { compute, result, ready } = useWasmCompute({ wasmUrl });
useWorkerNotifications const { progress, eventHistory } = useWorkerNotifications(worker);
useLLMMetadata useLLMMetadata({ route: pathname, mode: 'auto-extract' });
useRefPrint const { print } = useRefPrint(printRef, { documentTitle: 'Report' });
useRBAC const { can, hasRole, roles } = useRBAC({ userSource, roleDefinitions, roleCapabilities });

Usage Examples

useTransition

import { useTransition } from "preact-missing-hooks";

function ExampleTransition() {
  const [startTransition, isPending] = useTransition();

  const handleClick = () => {
    startTransition(() => {
      // perform an expensive update here
    });
  };

  return (
    <button onClick={handleClick} disabled={isPending}>
      {isPending ? "Loading..." : "Click Me"}
    </button>
  );
}

useMutationObserver

import { useRef } from "preact/hooks";
import { useMutationObserver } from "preact-missing-hooks";

function ExampleMutation() {
  const ref = useRef<HTMLDivElement>(null);

  useMutationObserver(
    ref,
    (mutations) => {
      console.log("Detected mutations:", mutations);
    },
    { childList: true, subtree: true }
  );

  return <div ref={ref}>Observe this content</div>;
}

useEventBus

// types.ts
export type Events = {
  notify: (message: string) => void;
};

// Sender.tsx
import { useEventBus } from "preact-missing-hooks";
import type { Events } from "./types";

function Sender() {
  const { emit } = useEventBus<Events>();
  return <button onClick={() => emit("notify", "Hello World!")}>Send</button>;
}

// Receiver.tsx
import { useEventBus } from "preact-missing-hooks";
import { useState, useEffect } from "preact/hooks";
import type { Events } from "./types";

function Receiver() {
  const [msg, setMsg] = useState<string>("");
  const { on } = useEventBus<Events>();

  useEffect(() => {
    const unsubscribe = on("notify", setMsg);
    return unsubscribe;
  }, []);

  return <div>Message: {msg}</div>;
}

useWrappedChildren

import { useWrappedChildren } from "preact-missing-hooks";

function ParentComponent({ children }) {
  // Inject common props into all children
  const injectProps = {
    className: "enhanced-child",
    onClick: () => console.log("Child clicked!"),
    style: { border: "1px solid #ccc" },
  };

  const wrappedChildren = useWrappedChildren(children, injectProps);

  return <div className="parent">{wrappedChildren}</div>;
}

// Usage with preserve strategy (default - existing props are preserved)
function PreserveExample() {
  return (
    <ParentComponent>
      <button className="btn">Existing class preserved</button>
      <span style={{ color: "red" }}>Both styles applied</span>
    </ParentComponent>
  );
}

// Usage with override strategy (injected props override existing ones)
function OverrideExample() {
  const injectProps = { className: "new-class" };
  const children = (
    <button className="old-class">Class will be overridden</button>
  );

  const wrappedChildren = useWrappedChildren(children, injectProps, "override");

  return <div>{wrappedChildren}</div>;
}

usePreferredTheme

import { usePreferredTheme } from "preact-missing-hooks";

function ThemeAwareComponent() {
  const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'

  return <div data-theme={theme}>Your system prefers: {theme}</div>;
}

useNetworkState

import { useNetworkState } from "preact-missing-hooks";

function NetworkStatus() {
  const { online, effectiveType, saveData } = useNetworkState();

  return (
    <div>
      Status: {online ? "Online" : "Offline"}
      {effectiveType && ` (${effectiveType})`}
      {saveData && " — Reduced data mode enabled"}
    </div>
  );
}

useDeviceData

import { useDeviceData } from "preact-missing-hooks";

function EnvironmentBadge() {
  const { browser, os, viewport } = useDeviceData({
    includeBattery: false,
    includeHighEntropy: true,
  });

  return (
    <span>
      {browser.name} {browser.version} · {os.name} {os.version} ·{" "}
      {viewport.width}×{viewport.height}
    </span>
  );
}

See full useDeviceData docs for all fields, getDeviceData(), and parseUserAgent().


useDebounce

Delays updating a value, or executing a function, until delay milliseconds have passed without further changes. Default delay: 300.

  • Value: returns the last value after the quiet period.
  • Function: returns a stable debounced callback with cancel() and flush().
import { useState, useEffect } from "preact/hooks";
import { useDebounce } from "preact-missing-hooks";

function SearchField() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebounce(query, 300);

  useEffect(() => {
    if (debouncedQuery) fetchResults(debouncedQuery);
  }, [debouncedQuery]);

  return <input value={query} onInput={(e) => setQuery(e.currentTarget.value)} />;
}

function Autosave({ text }) {
  const save = useDebounce((value: string) => {
    api.save(value);
  }, 400);

  return <textarea onInput={(e) => save(e.currentTarget.value)} />;
}

useIdle

Detects when the user has been inactive for timeout milliseconds (default: 60000). Activity is inferred from window events (mousemove, mousedown, keydown, touchstart, wheel, resize) and when the tab becomes visible again.

Returns idle, lastActive (timestamp), and reset() to treat the current moment as activity.

import { useIdle } from "preact-missing-hooks";

function IdleBanner() {
  const { idle, lastActive, reset } = useIdle(5000);

  return (
    <div>
      {idle ? "Away" : "Active"} · last active{" "}
      {new Date(lastActive).toLocaleTimeString()}
      <button onClick={reset}>I'm here</button>
    </div>
  );
}

useClickOutside

Calls handler when a pointer event happens outside the given element (or elements). Useful for closing dropdowns, modals, and popovers. Options: events (default mousedown + touchstart), enabled.

import { useRef, useState } from "preact/hooks";
import { useClickOutside } from "preact-missing-hooks";

function Dropdown() {
  const ref = useRef<HTMLDivElement>(null);
  const [open, setOpen] = useState(true);

  useClickOutside(ref, () => setOpen(false));

  if (!open) return <button onClick={() => setOpen(true)}>Open</button>;
  return <div ref={ref}>Menu</div>;
}

useMediaQuery

Tracks whether a CSS media query currently matches. Updates when the viewport or user preferences change. Optional second argument is the SSR / unavailable fallback (default: false).

import { useMediaQuery } from "preact-missing-hooks";

function ResponsiveNav() {
  const isMobile = useMediaQuery("(max-width: 768px)");
  const prefersDark = useMediaQuery("(prefers-color-scheme: dark)");

  return (
    <nav data-narrow={isMobile} data-theme={prefersDark ? "dark" : "light"}>
      {isMobile ? "Mobile nav" : "Desktop nav"}
    </nav>
  );
}

useClipboard

import { useState } from "preact/hooks";
import { useClipboard } from "preact-missing-hooks";

function CopyButton() {
  const { copy, copied, error } = useClipboard({ resetDelay: 2000 });

  return (
    <button onClick={() => copy("Hello, World!")}>
      {copied ? "Copied!" : "Copy"}
    </button>
  );
}

function PasteInput() {
  const [text, setText] = useState("");
  const { paste } = useClipboard();

  const handlePaste = async () => {
    const content = await paste();
    setText(content);
  };

  return (
    <div>
      <input value={text} onChange={(e) => setText(e.target.value)} />
      <button onClick={handlePaste}>Paste</button>
    </div>
  );
}

usePrefetch

Preload 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).

import { usePrefetch } from "preact-missing-hooks";

function NavLink({ href, children }) {
  const { prefetch, isPrefetched } = usePrefetch();
  return (
    <a href={href} onMouseEnter={() => prefetch(href)}>
      {children}
      {isPrefetched(href) && " ✓"}
    </a>
  );
}

// Prefetch API data
function DataLoader() {
  const { prefetch } = usePrefetch();
  prefetch("/api/user", { as: "fetch" });
  // ...
}

usePoll

Polls an async function at a fixed interval until it returns { done: true, data? }. Stops on error. Options: intervalMs, immediate, enabled. Returns data, done, error, pollCount, start, stop.

import { usePoll } from "preact-missing-hooks";

function StatusPoller() {
  const { data, done, error, pollCount, stop } = usePoll(
    async () => {
      const res = await fetch("/api/job/status");
      const json = await res.json();
      return json.ready ? { done: true, data: json } : { done: false };
    },
    { intervalMs: 1000, immediate: true }
  );

  if (error) return <div>Error: {error.message}</div>;
  if (done) return <div>Result: {JSON.stringify(data)}</div>;
  return (
    <div>
      Polling… ({pollCount} calls) <button onClick={stop}>Stop</button>
    </div>
  );
}

useDeviceData

Reads device and browser information from native APIs (navigator, screen, window, matchMedia). Detects browser and OS name/version via Client Hints when supported, with user-agent parsing as fallback. High-entropy hints (platformVersion, fullVersionList) refine versions when the browser allows it.

No permissions required for the base snapshot; battery uses the Battery Status API when available.

Options

Option Default Description
includeBattery true Poll navigator.getBattery() when available
batteryPollIntervalMs 60000 Battery refresh interval (ms)
includeHighEntropy true Request platformVersion / fullVersionList from Client Hints

Key return fields

Field Description
browser.name, browser.version Detected browser (e.g. Chrome, 120.0.0.0)
os.name, os.version Detected OS (e.g. Windows, 10.0)
language, languages Locale from navigator
screen, viewport Display and window size
hardwareConcurrency, deviceMemory CPU count, RAM hint (GB)
colorScheme, reducedMotion prefers-color-scheme / prefers-reduced-motion
userAgentData Raw Client Hints snapshot when available
battery Charging state and level (0–1) when enabled

Updates on resize, orientation, online/offline, and media preference changes.

import { useDeviceData, getDeviceData, parseUserAgent } from "preact-missing-hooks";

function DeviceInfo() {
  const device = useDeviceData({
    includeBattery: true,
    includeHighEntropy: true,
  });

  return (
    <section>
      <h2>Environment</h2>
      <p>
        {device.browser.name} {device.browser.version} on {device.os.name}{" "}
        {device.os.version}
      </p>
      <ul>
        <li>Language: {device.language}</li>
        <li>
          Viewport: {device.viewport.width}×{device.viewport.height}
        </li>
        <li>CPUs: {device.hardwareConcurrency ?? "—"}</li>
        <li>Theme: {device.colorScheme}</li>
        {device.battery && (
          <li>
            Battery: {Math.round(device.battery.level * 100)}%
            {device.battery.charging ? " (charging)" : ""}
          </li>
        )}
      </ul>
    </section>
  );
}

// One-off snapshot (e.g. analytics on page load)
const snapshot = getDeviceData();
sendAnalytics({
  browser: snapshot.browser.name,
  browserVersion: snapshot.browser.version,
  os: snapshot.os.name,
  osVersion: snapshot.os.version,
});

// Parse a custom UA string without the hook
const { browser, os } = parseUserAgent(customUserAgent);

useRageClick

Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to Sentry or your error tracker to surface rage-click issues and lower rage-click-related support.

import { useRef } from "preact/hooks";
import { useRageClick } from "preact-missing-hooks";

function SubmitButton() {
  const ref = useRef<HTMLButtonElement>(null);

  useRageClick(ref, {
    onRageClick: ({ count, event }) => {
      // Report to Sentry (or your error tracker) to create rage-click issues
      Sentry.captureMessage("Rage click detected", {
        level: "warning",
        extra: { count, target: event.target, tag: "rage_click" },
      });
    },
    threshold: 5, // min clicks (default 5, Sentry-style)
    timeWindow: 1000, // ms (default 1000)
    distanceThreshold: 30, // px (default 30)
  });

  return <button ref={ref}>Submit</button>;
}

useThreadedWorker

Runs 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.

import { useThreadedWorker } from "preact-missing-hooks";

// Sequential: one task at a time, sorted by priority
const sequential = useThreadedWorker(fetchUser, { mode: "sequential" });

// Parallel: up to N tasks at once
const parallel = useThreadedWorker(processItem, {
  mode: "parallel",
  concurrency: 4,
});

// API (same for both modes)
const {
  run, // (data, { priority?: number }) => Promise<TResult>
  loading, // true while any task is queued or running
  result, // last successful result
  error, // last error
  queueSize, // tasks queued + running
  clearQueue, // clear pending tasks (running continue)
  terminate, // clear queue and reject new run()
} = sequential;

// Run with priority (1 = highest)
await run({ userId: 1 }, { priority: 1 });
await run({ userId: 2 }, { priority: 3 });

useIndexedDB

Production-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.

Config: name, version, and tables (each table: keyPath, autoIncrement?, indexes?).

Table API: insert, update, delete, exists, query(filterFn), upsert, bulkInsert, clear, count.

Database API: db.table(name), db.hasTable(name), db.transaction(storeNames, mode, callback, options?).

import { useIndexedDB } from "preact-missing-hooks";

function App() {
  const { db, isReady, error } = useIndexedDB({
    name: "my-app-db",
    version: 1,
    tables: {
      users: { keyPath: "id", autoIncrement: true, indexes: ["email"] },
      settings: { keyPath: "key" },
    },
  });

  if (error) return <div>Failed to open database</div>;
  if (!isReady || !db) return <div>Loading...</div>;

  const users = db.table("users");

  // All operations return Promises and accept optional { onSuccess, onError }
  await users.insert({ email: "a@b.com", name: "Alice" });
  await users.update(1, { name: "Alice Smith" });
  const found = await users.query((u) => u.email.startsWith("a@"));
  const n = await users.count();
  await users.delete(1);
  await users.upsert({ id: 2, email: "b@b.com" });
  await users.bulkInsert([{ email: "c@b.com" }, { email: "d@b.com" }]);
  await users.clear();

  // Full transaction support
  await db.transaction(["users", "settings"], "readwrite", async (tx) => {
    await tx.table("users").insert({ email: "e@b.com" });
    await tx.table("settings").upsert({ key: "theme", value: "dark" });
  });

  return <div>DB ready. Tables: {db.hasTable("users") ? "users" : ""}</div>;
}

useWebRTCIP

Detects 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, ipify, ip-api.com).

Returns { ips: string[], loading: boolean, error: string | null }. Options: stunServers, timeout (ms), onDetect(ip).

import { useWebRTCIP } from "preact-missing-hooks";
import { useState, useEffect } from "preact/hooks";

function ClientIP() {
  const { ips, loading, error } = useWebRTCIP({
    timeout: 4000,
    onDetect: (ip) => {
      /* optional: e.g. analytics */
    },
  });
  const [fallbackIP, setFallbackIP] = useState<string | null>(null);

  // Fallback to public IP API when WebRTC fails or returns empty
  useEffect(() => {
    if (loading || ips.length > 0) return;
    if (error) {
      fetch("https://api.ipify.org?format=json")
        .then((r) => r.json())
        .then((d) => setFallbackIP(d.ip))
        .catch(() => {});
    }
  }, [loading, ips.length, error]);

  if (loading) return <p>Detecting IP…</p>;
  if (ips.length > 0) return <p>IPs (WebRTC): {ips.join(", ")}</p>;
  if (fallbackIP) return <p>IP (fallback API): {fallbackIP}</p>;
  if (error) return <p>WebRTC failed. Try fallback API.</p>;
  return null;
}

useWasmCompute

Runs 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.

Returns { 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).

import { useWasmCompute } from "preact-missing-hooks";

function AddWithWasm() {
  const { compute, result, loading, error, ready } = useWasmCompute<
    number,
    number
  >({
    wasmUrl: "/add.wasm",
    exportName: "add",
  });

  const handleClick = () => {
    if (ready) compute(2).then(() => {});
  };

  if (error) return <p>WASM unavailable: {error}</p>;
  if (!ready) return <p>Loading WASM…</p>;
  return (
    <div>
      <button onClick={handleClick} disabled={loading}>
        Add 2
      </button>
      {result != null && <p>Result: {result}</p>}
    </div>
  );
}

useWorkerNotifications

Listens 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 }.

Returns 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).

import { useWorkerNotifications } from "preact-missing-hooks";

function WorkerDashboard({ worker }) {
  const { progress, eventHistory } = useWorkerNotifications(worker, {
    maxHistory: 50,
  });

  return (
    <div>
      <p>
        Running: {progress.runningTasks.length} | Done:{" "}
        {progress.completedCount} | Failed: {progress.failedCount}
      </p>
      <p>
        Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
        {progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
        {progress.currentQueueSize}
      </p>
      <small>Events: {eventHistory.length}</small>
    </div>
  );
}

useLLMMetadata

Injects 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.

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).

API:

type OGType =
  | "website"
  | "article"
  | "profile"
  | "video.other"
  | "product"
  | "music.song"
  | "book";

interface LLMConfig {
  route: string;
  mode?: "manual" | "auto-extract";
  title?: string;
  description?: string;
  tags?: string[];
  canonicalUrl?: string; // absolute URL
  language?: string; // e.g. "en", "en-US"
  ogType?: OGType; // Open Graph type
  ogImage?: string; // absolute image URL
  ogImageAlt?: string;
  siteName?: string;
  author?: string;
  publishedTime?: string; // ISO date
  modifiedTime?: string; // ISO date
  robots?: string; // e.g. "index, follow"
  extra?: Record<string, string | number | boolean | string[]>;
}

function useLLMMetadata(config: LLMConfig | null | undefined): void;

Behavior:

  • When config is null or undefined: injects a minimal payload with route: "/" and generatedAt (no throw).
  • When config.route (or other deps) change: removes any existing <script data-llm="true">, then injects a new one.
  • Script tag: <script type="application/llm+json" data-llm="true"> with JSON payload. Only defined, safe fields are included.
  • Cacheable: If the generated payload is unchanged, the script is not replaced.
  • SSR-safe: No-op when typeof window === "undefined".
  • Cleans up on unmount (removes the script).

Modes:

  • manual (default): Uses title, description, tags, and any other config fields you pass.
  • auto-extract: Fills title, description, and outline from the DOM (document.title, visible <h1>/<h2>, first 3 visible <p>). You can still override with config. Ignores content inside nav, footer, script, style.

Example payload (rich):

{
  "route": "/blog/ai-hooks",
  "title": "AI Hooks in Preact",
  "description": "A short summary...",
  "tags": ["preact", "react", "hooks"],
  "outline": ["Intro", "Problem", "Solution"],
  "canonicalUrl": "https://example.com/blog/ai-hooks",
  "language": "en",
  "ogType": "article",
  "ogImage": "https://example.com/og.png",
  "siteName": "My Blog",
  "author": "Jane Doe",
  "publishedTime": "2025-02-14T10:00:00.000Z",
  "modifiedTime": "2025-02-14T12:00:00.000Z",
  "robots": "index, follow",
  "generatedAt": "2025-02-14T12:00:00.000Z"
}

Example: React Router

import { useLocation } from "react-router-dom";
import { useLLMMetadata } from "preact-missing-hooks"; // or "preact-missing-hooks/react"

function App() {
  const { pathname } = useLocation();
  useLLMMetadata({
    route: pathname,
    mode: "auto-extract",
    title: document.title,
    tags: ["my-app"],
  });
  return <Outlet />;
}

Example: Preact Router

import { useLocation } from "preact-router";
import { useLLMMetadata } from "preact-missing-hooks";

function App() {
  const [pathname] = useLocation();
  useLLMMetadata({
    route: pathname ?? "/",
    mode: "manual",
    title: "My Page",
    description: "Page description",
    tags: ["preact", "hooks"],
  });
  return <div>{/* your routes / children */}</div>;
}

useRefPrint

Binds 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).

import { useRef } from "preact/hooks";
import { useRefPrint } from "preact-missing-hooks";

function Report() {
  const printRef = useRef<HTMLDivElement>(null);
  const { print } = useRefPrint(printRef, {
    documentTitle: "Monthly Report",
    downloadAsPdf: true,
  });

  return (
    <div>
      <div ref={printRef}>
        <h1>Report content</h1>
        <p>Only this section is printed when you click Print.</p>
      </div>
      <button onClick={print}>Print / Save as PDF</button>
    </div>
  );
}

useRBAC

Frontend-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.

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.

import { useRBAC } from "preact-missing-hooks";

const roleDefinitions = [
  { role: "admin", condition: (u) => u?.role === "admin" },
  {
    role: "editor",
    condition: (u) => u?.role === "editor" || u?.role === "admin",
  },
  { role: "viewer", condition: (u) => !!u?.id },
];
const roleCapabilities = {
  admin: ["*"],
  editor: ["posts:edit", "posts:create", "posts:read"],
  viewer: ["posts:read"],
};

function App() {
  const { user, roles, capabilities, hasRole, can, setUserInStorage } = useRBAC(
    {
      userSource: { type: "localStorage", key: "user" },
      roleDefinitions,
      roleCapabilities,
    }
  );

  const login = (role) => {
    setUserInStorage(
      { id: 1, role, email: role + "@app.com" },
      "localStorage",
      "user"
    );
  };
  const logout = () => setUserInStorage(null, "localStorage", "user");

  return (
    <div>
      {!user ? (
        <div>
          <button onClick={() => login("admin")}>Login as Admin</button>
          <button onClick={() => login("editor")}>Login as Editor</button>
          <button onClick={() => login("viewer")}>Login as Viewer</button>
        </div>
      ) : (
        <div>
          <p>Roles: {roles.join(", ")}</p>
          {can("posts:edit") && <button>Edit post</button>}
          {can("*") && <button>Admin panel</button>}
          <button onClick={logout}>Logout</button>
        </div>
      )}
    </div>
  );
}

Built With


License

MIT © Prakhar Dubey


Contributing

Contributions are welcome! Please open issues or submit PRs with new hooks or improvements.

About

A lightweight, extensible collection of modern hooks built specifically for Preact (compatible with react too), focused on performance, advanced browser capabilities, and frontend-first architecture. Demo(https://preact-missing-hooks.vercel.app/)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages