https://github.com/prakhardubey2002/preact-missing-hooks
A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.
https://github.com/prakhardubey2002/preact-missing-hooks
hooks microbundle preact preact-hooks react-hooks typescript usetransition
Last synced: 3 months ago
JSON representation
A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.
- Host: GitHub
- URL: https://github.com/prakhardubey2002/preact-missing-hooks
- Owner: prakhardubey2002
- License: mit
- Created: 2025-05-26T06:51:55.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-05-28T15:04:12.000Z (about 1 year ago)
- Last Synced: 2025-06-07T12:48:18.389Z (12 months ago)
- Topics: hooks, microbundle, preact, preact-hooks, react-hooks, typescript, usetransition
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/preact-missing-hooks
- Size: 161 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: Readme.md
- License: LICENSE
Awesome Lists containing this project
README
# Preact Missing Hooks
If this package helps you, please consider dropping a star on the [GitHub repo](https://github.com/prakhardubey2002/Preact-Missing-Hooks).
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)`.
- **`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](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://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
```bash
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:
```ts
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.ts` → `resolve: { 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:
```ts
import { useThreadedWorker } from "preact-missing-hooks/useThreadedWorker";
import { useClipboard } from "preact-missing-hooks/useClipboard";
import { usePrefetch } from "preact-missing-hooks/usePrefetch";
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`, `useRageClick`, `useThreadedWorker`, `useIndexedDB`, `useWebRTCIP`, `useWasmCompute`, `useWorkerNotifications`, `useLLMMetadata`, `useRefPrint`, `useRBAC`.
---
## Quick start
Minimal example (Preact or React):
```tsx
import {
useTransition,
useClipboard,
usePreferredTheme,
} from "preact-missing-hooks";
function App() {
const [startTransition, isPending] = useTransition();
const { copy, copied } = useClipboard();
const theme = usePreferredTheme();
return (
startTransition(() => {
/* heavy update */
})
}
disabled={isPending}
>
{isPending ? "Loading…" : "Update"}
copy("Hello!")}>
{copied ? "Copied!" : "Copy"}
Theme: {theme}
);
}
```
**Live demo:** Try every hook with live examples:
- **Online:** [preact-missing-hooks.vercel.app](https://preact-missing-hooks.vercel.app/)
- **Local:** Run the docs demo:
```bash
npm run build && npx serve -l 5000
# Open http://localhost:5000/docs/
```
Or open `docs/index.html` after building (see [docs/README.md](docs/README.md) for details).
**Usage at a glance:**
| Hook | One-liner |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [useTransition](#usetransition) | `const [startTransition, isPending] = useTransition();` |
| [useMutationObserver](#usemutationobserver) | `useMutationObserver(ref, callback, { childList: true });` |
| [useEventBus](#useeventbus) | `const { emit, on } = useEventBus();` |
| [useWrappedChildren](#usewrappedchildren) | `const wrapped = useWrappedChildren(children, { className: 'x' });` |
| [usePreferredTheme](#usepreferredtheme) | `const theme = usePreferredTheme(); // 'light' \| 'dark' \| 'no-preference'` |
| [useNetworkState](#usenetworkstate) | `const { online, effectiveType } = useNetworkState();` |
| [usePrefetch](#useprefetch) | `const { prefetch, isPrefetched } = usePrefetch();` |
| [useClipboard](#useclipboard) | `const { copy, paste, copied } = useClipboard();` |
| [useRageClick](#userageclick) | `useRageClick(ref, { onRageClick, threshold: 5 });` |
| [useThreadedWorker](#usethreadedworker) | `const { run, loading, result } = useThreadedWorker(fn, { mode: 'sequential' });` |
| [useIndexedDB](#useindexeddb) | `const { db, isReady } = useIndexedDB({ name, version, tables });` |
| [useWebRTCIP](#usewebrtcip) | `const { ips, loading, error } = useWebRTCIP({ timeout: 3000 });` |
| [useWasmCompute](#usewasmcompute) | `const { compute, result, ready } = useWasmCompute({ wasmUrl });` |
| [useWorkerNotifications](#useworkernotifications) | `const { progress, eventHistory } = useWorkerNotifications(worker);` |
| [useLLMMetadata](#usellmmetadata) | `useLLMMetadata({ route: pathname, mode: 'auto-extract' });` |
| [useRefPrint](#userefprint) | `const { print } = useRefPrint(printRef, { documentTitle: 'Report' });` |
| [useRBAC](#userbac) | `const { can, hasRole, roles } = useRBAC({ userSource, roleDefinitions, roleCapabilities });` |
---
## Usage Examples
### `useTransition`
```tsx
import { useTransition } from "preact-missing-hooks";
function ExampleTransition() {
const [startTransition, isPending] = useTransition();
const handleClick = () => {
startTransition(() => {
// perform an expensive update here
});
};
return (
{isPending ? "Loading..." : "Click Me"}
);
}
```
---
### `useMutationObserver`
```tsx
import { useRef } from "preact/hooks";
import { useMutationObserver } from "preact-missing-hooks";
function ExampleMutation() {
const ref = useRef(null);
useMutationObserver(
ref,
(mutations) => {
console.log("Detected mutations:", mutations);
},
{ childList: true, subtree: true }
);
return
Observe this content;
}
```
---
### `useEventBus`
```tsx
// 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();
return emit("notify", "Hello World!")}>Send;
}
// 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("");
const { on } = useEventBus();
useEffect(() => {
const unsubscribe = on("notify", setMsg);
return unsubscribe;
}, []);
return
Message: {msg};
}
```
---
### `useWrappedChildren`
```tsx
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
{wrappedChildren};
}
// Usage with preserve strategy (default - existing props are preserved)
function PreserveExample() {
return (
Existing class preserved
Both styles applied
);
}
// Usage with override strategy (injected props override existing ones)
function OverrideExample() {
const injectProps = { className: "new-class" };
const children = (
Class will be overridden
);
const wrappedChildren = useWrappedChildren(children, injectProps, "override");
return
{wrappedChildren};
}
```
---
### `usePreferredTheme`
```tsx
import { usePreferredTheme } from "preact-missing-hooks";
function ThemeAwareComponent() {
const theme = usePreferredTheme(); // 'light' | 'dark' | 'no-preference'
return
Your system prefers: {theme};
}
```
---
### `useNetworkState`
```tsx
import { useNetworkState } from "preact-missing-hooks";
function NetworkStatus() {
const { online, effectiveType, saveData } = useNetworkState();
return (
Status: {online ? "Online" : "Offline"}
{effectiveType && ` (${effectiveType})`}
{saveData && " — Reduced data mode enabled"}
);
}
```
---
### `useClipboard`
```tsx
import { useState } from "preact/hooks";
import { useClipboard } from "preact-missing-hooks";
function CopyButton() {
const { copy, copied, error } = useClipboard({ resetDelay: 2000 });
return (
copy("Hello, World!")}>
{copied ? "Copied!" : "Copy"}
);
}
function PasteInput() {
const [text, setText] = useState("");
const { paste } = useClipboard();
const handlePaste = async () => {
const content = await paste();
setText(content);
};
return (
setText(e.target.value)} />
Paste
);
}
```
---
### `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).
```tsx
import { usePrefetch } from "preact-missing-hooks";
function NavLink({ href, children }) {
const { prefetch, isPrefetched } = usePrefetch();
return (
prefetch(href)}>
{children}
{isPrefetched(href) && " ✓"}
);
}
// Prefetch API data
function DataLoader() {
const { prefetch } = usePrefetch();
prefetch("/api/user", { as: "fetch" });
// ...
}
```
---
### `useRageClick`
Detects rage clicks (multiple rapid clicks in the same area), e.g. when the UI is unresponsive. Report them to [Sentry](https://docs.sentry.io/product/issues/issue-details/replay-issues/rage-clicks/) or your error tracker to surface rage-click issues and lower rage-click-related support.
```tsx
import { useRef } from "preact/hooks";
import { useRageClick } from "preact-missing-hooks";
function SubmitButton() {
const ref = useRef(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 Submit;
}
```
---
### `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.
```tsx
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
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?)`.
```tsx
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
Failed to open database;
if (!isReady || !db) return Loading...;
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
DB ready. Tables: {db.hasTable("users") ? "users" : ""};
}
```
---
### `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](https://ipapi.co), [ipify](https://www.ipify.org), [ip-api.com](https://ip-api.com)).
Returns `{ ips: string[], loading: boolean, error: string | null }`. Options: `stunServers`, `timeout` (ms), `onDetect(ip)`.
```tsx
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(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
Detecting IP…
;
if (ips.length > 0) return IPs (WebRTC): {ips.join(", ")}
;
if (fallbackIP) return IP (fallback API): {fallbackIP}
;
if (error) return WebRTC failed. Try fallback API.
;
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).
```tsx
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
WASM unavailable: {error}
;
if (!ready) return Loading WASM…
;
return (
Add 2
{result != null && Result: {result}
}
);
}
```
---
### `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).
```tsx
import { useWorkerNotifications } from "preact-missing-hooks";
function WorkerDashboard({ worker }) {
const { progress, eventHistory } = useWorkerNotifications(worker, {
maxHistory: 50,
});
return (
Running: {progress.runningTasks.length} | Done:{" "}
{progress.completedCount} | Failed: {progress.failedCount}
Avg: {progress.averageDurationMs.toFixed(0)}ms | Throughput:{" "}
{progress.throughputPerSecond.toFixed(2)}/s | Queue:{" "}
{progress.currentQueueSize}
Events: {eventHistory.length}
);
}
```
---
### `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:**
```ts
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;
}
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 ``, 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):**
```json
{
"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**
```tsx
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**
```tsx
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).
```tsx
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.
```tsx
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
- [Preact](https://preactjs.com)
- [Microbundle](https://github.com/developit/microbundle)
- [TypeScript](https://www.typescriptlang.org)
- [Vitest](https://vitest.dev) for testing
---
## License
MIT © [Prakhar Dubey](https://github.com/prakhardubey2002)
---
## Contributing
Contributions are welcome! Please open issues or submit PRs with new hooks or improvements.