preact-missing-hooks
Version:
A lightweight, extendable collection of missing React-like hooks for Preact — plus fresh, powerful new ones designed specifically for modern Preact apps.
221 lines (178 loc) • 10.3 kB
Markdown
# Preact Missing Hooks — Demo & Usage
This folder contains a **live demo** of all hooks from `preact-missing-hooks`. Each hook is shown with a short flow description, example code, and an interactive “Live” panel.
## Running the demo
### Option 1: Local build + static server (recommended)
From the project root:
```bash
npm run build
npx serve -l 5000
```
Then open **http://localhost:5000/docs/** in your browser. The demo will load the built package from `../dist/index.module.js`.
### Option 2: Demo script
From the project root you can use the existing demo script:
```bash
npm run demo
```
Then open **http://localhost:5000/docs/** (or the port shown).
### Option 3: Unpkg (no build)
Open `docs/index.html` in a browser that supports ES modules and import maps. The demo will load Preact and `preact-missing-hooks` from unpkg. Some features (e.g. `useWasmCompute` with a local WASM file) may be limited when opened as a file.
---
## What the demo includes
| Hook | What you can try |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **useTransition** | Deferred state update; button shows “Pending…” then count. |
| **useMutationObserver** | Add a node in the observed area; see mutation count. |
| **useEventBus** | Emit “greet” and see the message in another part of the UI. |
| **useWrappedChildren** | Children buttons get injected styles. |
| **usePreferredTheme** | Shows light / dark / no-preference from system. |
| **useNetworkState** | Online/offline and connection type. |
| **useDeviceData** | Live browser/OS name & version (Client Hints + UA parse), language, CPUs, memory, viewport, screen, touch, theme prefs, UA brands, optional battery. |
| **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
| **usePoll** | Poll until done (3 ticks); see poll count and result. Stop button to cancel. |
| **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
| **useRageClick** | Click the area 3+ times quickly; rage click count. |
| **useThreadedWorker** | Run a task; see loading and result. |
| **useIndexedDB** | Insert, bulk insert, query, update, delete, clear. |
| **useWebRTCIP** | Detect IP via WebRTC (may take a few seconds). |
| **useWasmCompute** | Run WASM in a worker (needs `add.wasm` in docs). |
| **useWorkerNotifications** | Run/fail tasks and queue updates; toasts show events. |
| **useRefPrint** | Bind a ref to a section and click “Print / Save as PDF”; only that section is printed via `@media print`. |
| **useRBAC** | Login as Admin / Editor / Viewer (localStorage or sessionStorage); see roles and capabilities; conditional UI by `can(...)`. |
| **useLLMMetadata** | Change “route” with buttons; see injected script info in the Live panel and `<script data-llm="true">` in the document head. Safe with `null`/`undefined` config (minimal payload with `route: "/"`). |
---
## Demo usage patterns
### Minimal Preact example
```js
import { h, render } from "preact";
import { useState } from "preact/hooks";
import { useClipboard, usePreferredTheme } from "preact-missing-hooks";
function App() {
const { copy, copied } = useClipboard();
const theme = usePreferredTheme();
return h(
"div",
{},
h("button", { onClick: () => copy("Hi") }, copied ? "Copied!" : "Copy"),
h("span", {}, " Theme: " + theme)
);
}
render(h(App), document.getElementById("root"));
```
### With React
```jsx
import { useTransition, useLLMMetadata } from "preact-missing-hooks/react";
import { useLocation } from "react-router-dom";
function App() {
const { pathname } = useLocation();
useLLMMetadata({ route: pathname, mode: "auto-extract", tags: ["my-app"] });
const [startTransition, isPending] = useTransition();
// ...
}
```
### useRefPrint example
Print only a specific section via the native print dialog (user can save as PDF):
```js
import { h, render } from "preact";
import { useRef } from "preact/hooks";
import { useRefPrint } from "preact-missing-hooks";
function Report() {
const printRef = useRef(null);
const { print } = useRefPrint(printRef, {
documentTitle: "My Report",
downloadAsPdf: true,
});
return h(
"div",
{},
h(
"div",
{ ref: printRef, style: { padding: "1rem", background: "#f5f5f5" } },
"Only this section is printed when you click the button."
),
h("button", { onClick: print }, "Print / Save as PDF")
);
}
render(h(Report), document.getElementById("root"));
```
### useRBAC example
Frontend-only role-based access: define roles with conditions, assign capabilities per role, and read the current user from localStorage (or sessionStorage / API):
```js
import { h, render } from "preact";
import { useRBAC } from "preact-missing-hooks";
const roleDefinitions = [
{ role: "admin", condition: (u) => u && u.role === "admin" },
{
role: "editor",
condition: (u) => u && (u.role === "editor" || u.role === "admin"),
},
{ role: "viewer", condition: (u) => u && u.id != null },
];
const roleCapabilities = {
admin: ["*"],
editor: ["posts:edit", "posts:create", "posts:read"],
viewer: ["posts:read"],
};
function App() {
const { user, roles, can, setUserInStorage } = useRBAC({
userSource: { type: "localStorage", key: "app-user" },
roleDefinitions,
roleCapabilities,
});
if (!user) {
return h(
"div",
{},
h(
"button",
{
onClick: () =>
setUserInStorage(
{ id: 1, role: "admin" },
"localStorage",
"app-user"
),
},
"Login as Admin"
),
h(
"button",
{
onClick: () =>
setUserInStorage(
{ id: 2, role: "viewer" },
"localStorage",
"app-user"
),
},
"Login as Viewer"
)
);
}
return h(
"div",
{},
h("p", {}, "Roles: " + roles.join(", ")),
can("posts:edit") && h("button", {}, "Edit post"),
can("*") && h("button", {}, "Admin panel"),
h(
"button",
{ onClick: () => setUserInStorage(null, "localStorage", "app-user") },
"Logout"
)
);
}
render(h(App), document.getElementById("root"));
```
### useLLMMetadata in the demo
The **useLLMMetadata** Live panel simulates route changes. Click “Route: /”, “Route: /blog”, or “Route: /docs”. Each change:
1. Removes any existing `<script data-llm="true">`.
2. Injects a new script with `type="application/llm+json"` containing `route`, `title`, `description`, `tags`, and `generatedAt`.
Inspect the page’s `<head>` in DevTools to see the script. The Live panel shows a short summary of the injected payload. The hook never throws: passing `null` or `undefined` as config is safe and yields a minimal payload with `route: "/"` and `generatedAt`.
---
## File layout
- **index.html** — Page shell, styles, and import map; mounts the app into `#root`.
- **main.js** — Imports the package and Preact, defines demo components and the `HOOKS` list, renders the app.
To add a new hook to the demo:
1. Import it in `main.js` from the package.
2. Implement a `DemoXxx` component that uses the hook.
3. Add an entry to the `HOOKS` array with `name`, `flow`, `summary`, `code`, and `Live: DemoXxx`.