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