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.
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. Returnsprefetch(url, options?)andisPrefetched(url).usePoll— Polls an async function at a fixed interval until it returns{ done: true, data? }. Stops on error. Returnsdata,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, andmatchMediaAPIs (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 withcancel()/flush().useIdle— Detects when a user has been inactive for a specified period of time. Returnsidle,lastActive, andreset().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, optionalonSuccess/onErrorcallbacks.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 returnscompute(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 poststask_start/task_end/task_fail/queue_size; returnsprogress(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 (fromdocument.title, visibleh1/h2, first 3p). Cacheable, SSR-safe, no router dependency.useRefPrint— Binds a ref to a printable section and providesprint()to open the native print dialog. Uses@media printCSS 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. Returnsuser,roles,capabilities,hasRole(role),can(capability), and storage helpers.- Fully TypeScript compatible
- Bundled with Microbundle
- Zero dependencies (peer:
preactorreact— use/reactfor React)
npm install preact-missing-hooksEnsure your app has either preact or react installed (the package uses whichever is present).
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
preactorreactis installed and uses that build automatically. - ESM (Vite, Webpack, etc.): Default is the Preact build. In a React app, add the
reactcondition so the package resolves to the React build:- Vite:
vite.config.ts→resolve: { conditions: ['react'] } - Webpack:
resolve.conditionNames(or similar) to include'react'
- Vite:
- Or in React projects you can always import from the explicit entry:
preact-missing-hooks/react.
- CommonJS / Node: The package detects which of
-
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.
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:
- Online: preact-missing-hooks.vercel.app
- Local: Run the docs demo:
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 }); |
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>
);
}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>;
}// 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>;
}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>;
}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>;
}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>
);
}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().
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()andflush().
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)} />;
}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>
);
}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>;
}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>
);
}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>
);
}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" });
// ...
}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>
);
}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);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>;
}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 });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>;
}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;
}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>
);
}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>
);
}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
configisnullorundefined: injects a minimal payload withroute: "/"andgeneratedAt(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): Usestitle,description,tags, and any other config fields you pass.auto-extract: Fillstitle,description, andoutlinefrom the DOM (document.title, visible<h1>/<h2>, first 3 visible<p>). You can still override with config. Ignores content insidenav,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>;
}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>
);
}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>
);
}- Preact
- Microbundle
- TypeScript
- Vitest for testing
MIT © Prakhar Dubey
Contributions are welcome! Please open issues or submit PRs with new hooks or improvements.