preact-missing-hooks 4.5.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +176 -17
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +1 -1
- package/dist/index.modern.mjs.map +1 -1
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/react.js +155 -0
- package/dist/usePoll.d.ts +47 -0
- package/dist/usePrefetch.d.ts +44 -0
- package/docs/README.md +108 -0
- package/docs/main.js +65 -0
- package/llm.package.json +93 -219
- package/llm.package.txt +32 -48
- package/package.json +14 -2
- package/src/index.ts +2 -0
- package/src/usePoll.ts +110 -0
- package/src/usePrefetch.ts +92 -0
- package/tests/usePoll.test.tsx +110 -0
- package/tests/usePrefetch.test.tsx +107 -0
package/dist/react.js
CHANGED
|
@@ -2008,13 +2008,168 @@ function useRBAC(options) {
|
|
|
2008
2008
|
};
|
|
2009
2009
|
}
|
|
2010
2010
|
|
|
2011
|
+
function prefetchDocument(url) {
|
|
2012
|
+
if (typeof document === "undefined")
|
|
2013
|
+
return;
|
|
2014
|
+
const links = document.querySelectorAll('link[rel="prefetch"]');
|
|
2015
|
+
for (let i = 0; i < links.length; i++) {
|
|
2016
|
+
if (links[i].href === url)
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
const link = document.createElement("link");
|
|
2020
|
+
link.rel = "prefetch";
|
|
2021
|
+
link.href = url;
|
|
2022
|
+
document.head.appendChild(link);
|
|
2023
|
+
}
|
|
2024
|
+
function prefetchFetch(url) {
|
|
2025
|
+
if (typeof fetch === "undefined")
|
|
2026
|
+
return;
|
|
2027
|
+
fetch(url, { method: "GET", mode: "cors" }).catch(() => {
|
|
2028
|
+
/* ignore; prefetch is best-effort */
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
|
|
2033
|
+
* so they are cached before the user navigates or needs them. Useful for link hover or
|
|
2034
|
+
* route preloading.
|
|
2035
|
+
*
|
|
2036
|
+
* @returns Object with prefetch(url, options?) and isPrefetched(url)
|
|
2037
|
+
*
|
|
2038
|
+
* @example
|
|
2039
|
+
* ```tsx
|
|
2040
|
+
* function NavLink({ href, children }) {
|
|
2041
|
+
* const { prefetch } = usePrefetch();
|
|
2042
|
+
* return (
|
|
2043
|
+
* <a
|
|
2044
|
+
* href={href}
|
|
2045
|
+
* onMouseEnter={() => prefetch(href)}
|
|
2046
|
+
* >
|
|
2047
|
+
* {children}
|
|
2048
|
+
* </a>
|
|
2049
|
+
* );
|
|
2050
|
+
* }
|
|
2051
|
+
* ```
|
|
2052
|
+
*
|
|
2053
|
+
* @example
|
|
2054
|
+
* ```tsx
|
|
2055
|
+
* // Prefetch API data
|
|
2056
|
+
* const { prefetch } = usePrefetch();
|
|
2057
|
+
* prefetch('/api/user', { as: 'fetch' });
|
|
2058
|
+
* ```
|
|
2059
|
+
*/
|
|
2060
|
+
function usePrefetch() {
|
|
2061
|
+
const prefetchedRef = react.useRef(new Set());
|
|
2062
|
+
const [, setTick] = react.useState(0);
|
|
2063
|
+
const prefetch = react.useCallback((url, options) => {
|
|
2064
|
+
var _a;
|
|
2065
|
+
const trimmed = url === null || url === void 0 ? void 0 : url.trim();
|
|
2066
|
+
if (!trimmed)
|
|
2067
|
+
return;
|
|
2068
|
+
if (prefetchedRef.current.has(trimmed))
|
|
2069
|
+
return;
|
|
2070
|
+
const as = (_a = options === null || options === void 0 ? void 0 : options.as) !== null && _a !== void 0 ? _a : "document";
|
|
2071
|
+
if (as === "document") {
|
|
2072
|
+
prefetchDocument(trimmed);
|
|
2073
|
+
}
|
|
2074
|
+
else {
|
|
2075
|
+
prefetchFetch(trimmed);
|
|
2076
|
+
}
|
|
2077
|
+
prefetchedRef.current.add(trimmed);
|
|
2078
|
+
setTick((n) => n + 1);
|
|
2079
|
+
}, []);
|
|
2080
|
+
const isPrefetched = react.useCallback((url) => {
|
|
2081
|
+
var _a;
|
|
2082
|
+
return prefetchedRef.current.has((_a = url === null || url === void 0 ? void 0 : url.trim()) !== null && _a !== void 0 ? _a : "");
|
|
2083
|
+
}, []);
|
|
2084
|
+
return { prefetch, isPrefetched };
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
/**
|
|
2088
|
+
* Polls an async function at a fixed interval until it returns { done: true, data? }.
|
|
2089
|
+
* Useful for waiting on a backend job, readiness checks, or until a condition is met.
|
|
2090
|
+
*
|
|
2091
|
+
* @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
|
|
2092
|
+
* @param options - intervalMs, immediate, enabled
|
|
2093
|
+
* @returns { data, done, error, pollCount, start, stop }
|
|
2094
|
+
*
|
|
2095
|
+
* @example
|
|
2096
|
+
* ```tsx
|
|
2097
|
+
* const { data, done, pollCount } = usePoll(
|
|
2098
|
+
* async () => {
|
|
2099
|
+
* const res = await fetch('/api/status');
|
|
2100
|
+
* const json = await res.json();
|
|
2101
|
+
* return json.ready ? { done: true, data: json } : { done: false };
|
|
2102
|
+
* },
|
|
2103
|
+
* { intervalMs: 500, immediate: true }
|
|
2104
|
+
* );
|
|
2105
|
+
* return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
|
|
2106
|
+
* ```
|
|
2107
|
+
*/
|
|
2108
|
+
function usePoll(pollFn, options = {}) {
|
|
2109
|
+
const { intervalMs = 1000, immediate = true, enabled = true } = options;
|
|
2110
|
+
const [data, setData] = react.useState(null);
|
|
2111
|
+
const [done, setDone] = react.useState(false);
|
|
2112
|
+
const [error, setError] = react.useState(null);
|
|
2113
|
+
const [pollCount, setPollCount] = react.useState(0);
|
|
2114
|
+
const [running, setRunning] = react.useState(false);
|
|
2115
|
+
const intervalRef = react.useRef(null);
|
|
2116
|
+
const pollFnRef = react.useRef(pollFn);
|
|
2117
|
+
pollFnRef.current = pollFn;
|
|
2118
|
+
const stop = react.useCallback(() => {
|
|
2119
|
+
if (intervalRef.current != null) {
|
|
2120
|
+
clearInterval(intervalRef.current);
|
|
2121
|
+
intervalRef.current = null;
|
|
2122
|
+
}
|
|
2123
|
+
setRunning(false);
|
|
2124
|
+
}, []);
|
|
2125
|
+
const tick = react.useCallback(() => __awaiter(this, void 0, void 0, function* () {
|
|
2126
|
+
try {
|
|
2127
|
+
setError(null);
|
|
2128
|
+
const result = yield pollFnRef.current();
|
|
2129
|
+
setPollCount((n) => n + 1);
|
|
2130
|
+
if (result.done) {
|
|
2131
|
+
stop();
|
|
2132
|
+
setDone(true);
|
|
2133
|
+
if (result.data !== undefined) {
|
|
2134
|
+
setData(result.data);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
catch (e) {
|
|
2139
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
2140
|
+
setError(err);
|
|
2141
|
+
stop();
|
|
2142
|
+
}
|
|
2143
|
+
}), [stop]);
|
|
2144
|
+
const start = react.useCallback(() => {
|
|
2145
|
+
if (!enabled || running)
|
|
2146
|
+
return;
|
|
2147
|
+
setRunning(true);
|
|
2148
|
+
if (immediate) {
|
|
2149
|
+
tick();
|
|
2150
|
+
}
|
|
2151
|
+
intervalRef.current = setInterval(tick, intervalMs);
|
|
2152
|
+
}, [enabled, immediate, intervalMs, running, tick]);
|
|
2153
|
+
react.useEffect(() => {
|
|
2154
|
+
if (enabled) {
|
|
2155
|
+
start();
|
|
2156
|
+
}
|
|
2157
|
+
return () => {
|
|
2158
|
+
stop();
|
|
2159
|
+
};
|
|
2160
|
+
}, [enabled]);
|
|
2161
|
+
return { data, done, error, pollCount, start, stop };
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2011
2164
|
exports.useClipboard = useClipboard;
|
|
2012
2165
|
exports.useEventBus = useEventBus;
|
|
2013
2166
|
exports.useIndexedDB = useIndexedDB;
|
|
2014
2167
|
exports.useLLMMetadata = useLLMMetadata;
|
|
2015
2168
|
exports.useMutationObserver = useMutationObserver;
|
|
2016
2169
|
exports.useNetworkState = useNetworkState;
|
|
2170
|
+
exports.usePoll = usePoll;
|
|
2017
2171
|
exports.usePreferredTheme = usePreferredTheme;
|
|
2172
|
+
exports.usePrefetch = usePrefetch;
|
|
2018
2173
|
exports.useRBAC = useRBAC;
|
|
2019
2174
|
exports.useRageClick = useRageClick;
|
|
2020
2175
|
exports.useRefPrint = useRefPrint;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface UsePollOptions {
|
|
2
|
+
/** Polling interval in milliseconds. Default: 1000 */
|
|
3
|
+
intervalMs?: number;
|
|
4
|
+
/** Run the poll function immediately when the hook mounts. Default: true */
|
|
5
|
+
immediate?: boolean;
|
|
6
|
+
/** When false, do not start or continue polling. Default: true */
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface UsePollResult<T> {
|
|
10
|
+
/** Last resolved data when poll returned done: true */
|
|
11
|
+
data: T | null;
|
|
12
|
+
/** True once the poll function returned { done: true } */
|
|
13
|
+
done: boolean;
|
|
14
|
+
/** Error from the last failed poll call */
|
|
15
|
+
error: Error | null;
|
|
16
|
+
/** Number of times the poll function has been invoked */
|
|
17
|
+
pollCount: number;
|
|
18
|
+
/** Manually start polling (e.g. after reset). Only has effect when not already polling. */
|
|
19
|
+
start: () => void;
|
|
20
|
+
/** Stop polling. */
|
|
21
|
+
stop: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Polls an async function at a fixed interval until it returns { done: true, data? }.
|
|
25
|
+
* Useful for waiting on a backend job, readiness checks, or until a condition is met.
|
|
26
|
+
*
|
|
27
|
+
* @param pollFn - Async function called each tick. Return { done: true, data? } to stop and set result.
|
|
28
|
+
* @param options - intervalMs, immediate, enabled
|
|
29
|
+
* @returns { data, done, error, pollCount, start, stop }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { data, done, pollCount } = usePoll(
|
|
34
|
+
* async () => {
|
|
35
|
+
* const res = await fetch('/api/status');
|
|
36
|
+
* const json = await res.json();
|
|
37
|
+
* return json.ready ? { done: true, data: json } : { done: false };
|
|
38
|
+
* },
|
|
39
|
+
* { intervalMs: 500, immediate: true }
|
|
40
|
+
* );
|
|
41
|
+
* return done ? <div>Ready: {JSON.stringify(data)}</div> : <div>Polling… ({pollCount})</div>;
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function usePoll<T>(pollFn: () => Promise<{
|
|
45
|
+
done: boolean;
|
|
46
|
+
data?: T;
|
|
47
|
+
}>, options?: UsePollOptions): UsePollResult<T>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface UsePrefetchOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Resource type for prefetch.
|
|
4
|
+
* - "document": uses <link rel="prefetch"> (default, for next navigation)
|
|
5
|
+
* - "fetch": uses fetch() to warm the HTTP cache (e.g. for API or same-origin data)
|
|
6
|
+
*/
|
|
7
|
+
as?: "document" | "fetch";
|
|
8
|
+
}
|
|
9
|
+
export interface UsePrefetchReturn {
|
|
10
|
+
/** Prefetch a URL so it is cached for later use. No-op if URL was already prefetched or empty. */
|
|
11
|
+
prefetch: (url: string, options?: UsePrefetchOptions) => void;
|
|
12
|
+
/** Check whether a URL has already been prefetched in this hook instance. */
|
|
13
|
+
isPrefetched: (url: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A Preact hook that returns a stable prefetch function to preload URLs (documents or data)
|
|
17
|
+
* so they are cached before the user navigates or needs them. Useful for link hover or
|
|
18
|
+
* route preloading.
|
|
19
|
+
*
|
|
20
|
+
* @returns Object with prefetch(url, options?) and isPrefetched(url)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* function NavLink({ href, children }) {
|
|
25
|
+
* const { prefetch } = usePrefetch();
|
|
26
|
+
* return (
|
|
27
|
+
* <a
|
|
28
|
+
* href={href}
|
|
29
|
+
* onMouseEnter={() => prefetch(href)}
|
|
30
|
+
* >
|
|
31
|
+
* {children}
|
|
32
|
+
* </a>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // Prefetch API data
|
|
40
|
+
* const { prefetch } = usePrefetch();
|
|
41
|
+
* prefetch('/api/user', { as: 'fetch' });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function usePrefetch(): UsePrefetchReturn;
|
package/docs/README.md
CHANGED
|
@@ -41,6 +41,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
|
|
|
41
41
|
| **useWrappedChildren** | Children buttons get injected styles. |
|
|
42
42
|
| **usePreferredTheme** | Shows light / dark / no-preference from system. |
|
|
43
43
|
| **useNetworkState** | Online/offline and connection type. |
|
|
44
|
+
| **usePrefetch** | Hover or click to prefetch a URL (document or fetch); see prefetched status. |
|
|
45
|
+
| **usePoll** | Poll until done (3 ticks); see poll count and result. Stop button to cancel. |
|
|
44
46
|
| **useClipboard** | Copy and paste; see “Copied!” and pasted text. |
|
|
45
47
|
| **useRageClick** | Click the area 3+ times quickly; rage click count. |
|
|
46
48
|
| **useThreadedWorker** | Run a task; see loading and result. |
|
|
@@ -48,6 +50,8 @@ Open `docs/index.html` in a browser that supports ES modules and import maps. Th
|
|
|
48
50
|
| **useWebRTCIP** | Detect IP via WebRTC (may take a few seconds). |
|
|
49
51
|
| **useWasmCompute** | Run WASM in a worker (needs `add.wasm` in docs). |
|
|
50
52
|
| **useWorkerNotifications** | Run/fail tasks and queue updates; toasts show events. |
|
|
53
|
+
| **useRefPrint** | Bind a ref to a section and click “Print / Save as PDF”; only that section is printed via `@media print`. |
|
|
54
|
+
| **useRBAC** | Login as Admin / Editor / Viewer (localStorage or sessionStorage); see roles and capabilities; conditional UI by `can(...)`. |
|
|
51
55
|
| **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: "/"`). |
|
|
52
56
|
|
|
53
57
|
---
|
|
@@ -88,6 +92,110 @@ function App() {
|
|
|
88
92
|
}
|
|
89
93
|
```
|
|
90
94
|
|
|
95
|
+
### useRefPrint example
|
|
96
|
+
|
|
97
|
+
Print only a specific section via the native print dialog (user can save as PDF):
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { h, render } from "preact";
|
|
101
|
+
import { useRef } from "preact/hooks";
|
|
102
|
+
import { useRefPrint } from "preact-missing-hooks";
|
|
103
|
+
|
|
104
|
+
function Report() {
|
|
105
|
+
const printRef = useRef(null);
|
|
106
|
+
const { print } = useRefPrint(printRef, {
|
|
107
|
+
documentTitle: "My Report",
|
|
108
|
+
downloadAsPdf: true,
|
|
109
|
+
});
|
|
110
|
+
return h(
|
|
111
|
+
"div",
|
|
112
|
+
{},
|
|
113
|
+
h(
|
|
114
|
+
"div",
|
|
115
|
+
{ ref: printRef, style: { padding: "1rem", background: "#f5f5f5" } },
|
|
116
|
+
"Only this section is printed when you click the button."
|
|
117
|
+
),
|
|
118
|
+
h("button", { onClick: print }, "Print / Save as PDF")
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
render(h(Report), document.getElementById("root"));
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### useRBAC example
|
|
125
|
+
|
|
126
|
+
Frontend-only role-based access: define roles with conditions, assign capabilities per role, and read the current user from localStorage (or sessionStorage / API):
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { h, render } from "preact";
|
|
130
|
+
import { useRBAC } from "preact-missing-hooks";
|
|
131
|
+
|
|
132
|
+
const roleDefinitions = [
|
|
133
|
+
{ role: "admin", condition: (u) => u && u.role === "admin" },
|
|
134
|
+
{
|
|
135
|
+
role: "editor",
|
|
136
|
+
condition: (u) => u && (u.role === "editor" || u.role === "admin"),
|
|
137
|
+
},
|
|
138
|
+
{ role: "viewer", condition: (u) => u && u.id != null },
|
|
139
|
+
];
|
|
140
|
+
const roleCapabilities = {
|
|
141
|
+
admin: ["*"],
|
|
142
|
+
editor: ["posts:edit", "posts:create", "posts:read"],
|
|
143
|
+
viewer: ["posts:read"],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function App() {
|
|
147
|
+
const { user, roles, can, setUserInStorage } = useRBAC({
|
|
148
|
+
userSource: { type: "localStorage", key: "app-user" },
|
|
149
|
+
roleDefinitions,
|
|
150
|
+
roleCapabilities,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!user) {
|
|
154
|
+
return h(
|
|
155
|
+
"div",
|
|
156
|
+
{},
|
|
157
|
+
h(
|
|
158
|
+
"button",
|
|
159
|
+
{
|
|
160
|
+
onClick: () =>
|
|
161
|
+
setUserInStorage(
|
|
162
|
+
{ id: 1, role: "admin" },
|
|
163
|
+
"localStorage",
|
|
164
|
+
"app-user"
|
|
165
|
+
),
|
|
166
|
+
},
|
|
167
|
+
"Login as Admin"
|
|
168
|
+
),
|
|
169
|
+
h(
|
|
170
|
+
"button",
|
|
171
|
+
{
|
|
172
|
+
onClick: () =>
|
|
173
|
+
setUserInStorage(
|
|
174
|
+
{ id: 2, role: "viewer" },
|
|
175
|
+
"localStorage",
|
|
176
|
+
"app-user"
|
|
177
|
+
),
|
|
178
|
+
},
|
|
179
|
+
"Login as Viewer"
|
|
180
|
+
)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return h(
|
|
184
|
+
"div",
|
|
185
|
+
{},
|
|
186
|
+
h("p", {}, "Roles: " + roles.join(", ")),
|
|
187
|
+
can("posts:edit") && h("button", {}, "Edit post"),
|
|
188
|
+
can("*") && h("button", {}, "Admin panel"),
|
|
189
|
+
h(
|
|
190
|
+
"button",
|
|
191
|
+
{ onClick: () => setUserInStorage(null, "localStorage", "app-user") },
|
|
192
|
+
"Logout"
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
render(h(App), document.getElementById("root"));
|
|
197
|
+
```
|
|
198
|
+
|
|
91
199
|
### useLLMMetadata in the demo
|
|
92
200
|
|
|
93
201
|
The **useLLMMetadata** Live panel simulates route changes. Click “Route: /”, “Route: /blog”, or “Route: /docs”. Each change:
|
package/docs/main.js
CHANGED
|
@@ -21,6 +21,8 @@ const {
|
|
|
21
21
|
useLLMMetadata,
|
|
22
22
|
useRefPrint,
|
|
23
23
|
useRBAC,
|
|
24
|
+
usePrefetch,
|
|
25
|
+
usePoll,
|
|
24
26
|
} = await import(
|
|
25
27
|
isLocal ? '../dist/index.module.js' : 'https://unpkg.com/preact-missing-hooks/dist/index.module.js'
|
|
26
28
|
);
|
|
@@ -84,6 +86,55 @@ function DemoNetworkState() {
|
|
|
84
86
|
);
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
function DemoPrefetch() {
|
|
90
|
+
const { prefetch, isPrefetched } = usePrefetch();
|
|
91
|
+
const [lastUrl, setLastUrl] = useState('');
|
|
92
|
+
const demoUrl = 'https://example.com/prefetched-page';
|
|
93
|
+
const fetchUrl = 'https://httpbin.org/get';
|
|
94
|
+
const doPrefetchDoc = () => { prefetch(demoUrl); setLastUrl(demoUrl); };
|
|
95
|
+
const doPrefetchFetch = () => { prefetch(fetchUrl, { as: 'fetch' }); setLastUrl(fetchUrl); };
|
|
96
|
+
return h('div', {},
|
|
97
|
+
h('div', { style: { marginBottom: '0.5rem', fontSize: '0.85rem' } }, [
|
|
98
|
+
h('button', {
|
|
99
|
+
onClick: doPrefetchDoc,
|
|
100
|
+
onMouseEnter: doPrefetchDoc,
|
|
101
|
+
}, 'Prefetch document'),
|
|
102
|
+
' ',
|
|
103
|
+
h('button', {
|
|
104
|
+
onClick: doPrefetchFetch,
|
|
105
|
+
onMouseEnter: doPrefetchFetch,
|
|
106
|
+
}, 'Prefetch (fetch)'),
|
|
107
|
+
]),
|
|
108
|
+
lastUrl
|
|
109
|
+
? h('span', { class: 'badge green', style: { marginLeft: '0.35rem' } },
|
|
110
|
+
'Prefetched: ' + lastUrl + (isPrefetched(lastUrl) ? ' ✓' : ''))
|
|
111
|
+
: h('span', { class: 'status' }, 'Hover or click to prefetch a URL (document or fetch).')
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function DemoPoll() {
|
|
116
|
+
const countRef = { current: 0 };
|
|
117
|
+
const { data, done, error, pollCount, start, stop } = usePoll(
|
|
118
|
+
async () => {
|
|
119
|
+
countRef.current += 1;
|
|
120
|
+
if (countRef.current >= 3) return { done: true, data: { message: 'Ready after ' + countRef.current + ' polls' } };
|
|
121
|
+
return { done: false };
|
|
122
|
+
},
|
|
123
|
+
{ intervalMs: 700, immediate: true }
|
|
124
|
+
);
|
|
125
|
+
return h('div', {},
|
|
126
|
+
h('div', { style: { marginBottom: '0.5rem', fontSize: '0.85rem' } }, [
|
|
127
|
+
h('button', { onClick: start }, 'Start'),
|
|
128
|
+
' ',
|
|
129
|
+
h('button', { onClick: stop }, 'Stop'),
|
|
130
|
+
]),
|
|
131
|
+
error ? h('span', { class: 'badge', style: { background: 'var(--red)', color: '#fff' } }, error.message) : null,
|
|
132
|
+
done
|
|
133
|
+
? h('span', { class: 'badge green', style: { marginLeft: '0.35rem' } }, data?.message ?? 'Done')
|
|
134
|
+
: h('span', { class: 'status' }, 'Polling… (' + pollCount + ' calls)')
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
87
138
|
function DemoClipboard() {
|
|
88
139
|
const { copy, paste, copied, error } = useClipboard();
|
|
89
140
|
const [pasted, setPasted] = useState('');
|
|
@@ -511,6 +562,20 @@ const HOOKS = [
|
|
|
511
562
|
code: `const state = useNetworkState();\n// state.online, state.effectiveType, ...`,
|
|
512
563
|
Live: DemoNetworkState,
|
|
513
564
|
},
|
|
565
|
+
{
|
|
566
|
+
name: 'usePrefetch',
|
|
567
|
+
flow: 'Component → usePrefetch() → prefetch(url, options?) → link rel=prefetch or fetch()',
|
|
568
|
+
summary: 'Preload URLs (documents or data) so they are cached before navigation or use. Ideal for link hover or route preloading.',
|
|
569
|
+
code: `const { prefetch, isPrefetched } = usePrefetch();\n<a onMouseEnter={() => prefetch(href)} href={href}>Link</a>\n// or prefetch(url, { as: 'fetch' }) for API`,
|
|
570
|
+
Live: DemoPrefetch,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: 'usePoll',
|
|
574
|
+
flow: 'Component → usePoll(pollFn, { intervalMs, immediate }) → poll until done: true → data, done, pollCount',
|
|
575
|
+
summary: 'Polls an async function at a fixed interval until it returns { done: true, data? }. Stops on error. Good for readiness checks or waiting on a backend job.',
|
|
576
|
+
code: `const { data, done, error, pollCount, stop } = usePoll(\n async () => (await fetch('/api/status')).ok ? { done: true, data } : { done: false },\n { intervalMs: 1000, immediate: true }\n);`,
|
|
577
|
+
Live: DemoPoll,
|
|
578
|
+
},
|
|
514
579
|
{
|
|
515
580
|
name: 'useClipboard',
|
|
516
581
|
flow: 'Component → useClipboard() → copy(text) / paste() → Clipboard API',
|