npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

preact-missing-hooks

v4.7.0

Published

A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.

Readme

Preact Missing Hooks

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, 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.
  • 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 { 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, 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 }); | | 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>
  );
}

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>
  );
}

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: "[email protected]", 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: "[email protected]" });
  await users.bulkInsert([{ email: "[email protected]" }, { email: "[email protected]" }]);
  await users.clear();

  // Full transaction support
  await db.transaction(["users", "settings"], "readwrite", async (tx) => {
    await tx.table("users").insert({ email: "[email protected]" });
    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.