@native-router/react 1.6.2 → 1.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 +1 -1
- package/dist/create-routes.cjs.map +1 -1
- package/dist/create-routes.js.map +1 -1
- package/dist/types/use-blocker.d.ts +59 -2
- package/dist/use-blocker.cjs +1 -1
- package/dist/use-blocker.cjs.map +1 -1
- package/dist/use-blocker.js +1 -1
- package/dist/use-blocker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ function Preview({visible}: {visible: boolean}) {
|
|
|
79
79
|
- Type-safe links: `createRoutes(routes)` checks the table while keeping every `path` literal, `RoutePaths<typeof routes>` extracts the pattern union(through nesting and param segments), and `<TypedLink<RoutePaths<...>> to params>` narrows `to` to the table and checks `params` against the exact pattern's segments — compile errors for unknown paths and missing/wrong params, click-time interpolation with encoding as the runtime backstop
|
|
80
80
|
- `ScrollRestoration` restores the scroll offset per history entry on back/forward and resets it on push (`resetOnPush` to opt out)
|
|
81
81
|
- Router-level `preload(router, to)` shares resolved views across links with in-flight dedup and a 30s TTL; `PrefetchLink` prefetch through it
|
|
82
|
-
- Hooks: `useRouter`, `useView`, `useData<T>(name?)` (typed data of the current level, or named data of ancestor routes), `useMatched` (matched levels, params, location), `useLoading`, `usePrefetch`, `useSearch(schema?)`, `useSetSearch(schema)`, `useBlocker(fn)` (unsaved-changes guard: the core `setBlocker` veto, registered while the component is mounted and always asked through the latest closure)
|
|
82
|
+
- Hooks: `useRouter`, `useView`, `useData<T>(name?)` (typed data of the current level, or named data of ancestor routes), `useMatched` (matched levels, params, location), `useLoading`, `usePrefetch`, `useSearch(schema?)`, `useSetSearch(schema)`, `useBlocker(fn)` (unsaved-changes guard: the core `setBlocker` veto, registered while the component is mounted and always asked through the latest closure; every veto is tracked on the returned `blocker.state` with a `proceed()`/`reset()` channel — `proceed()` retries the vetoed navigation bypassing this hook's blocker only, so the confirm dialog is a three-liner)
|
|
83
83
|
- Two error layers, both phases: global `errorHandler` prop on the Router, per-route `errorComponent` receiving `{error, ctx}` — `errorComponent` renders for resolve failures(loader/guard/search, no `ctx.phase`) AND for render errors thrown by the component subtree(`ctx.phase === 'render'`, caught by a route-level error boundary so a rendering crash never escapes past its route, like the browser's error page for any failed load)
|
|
84
84
|
- Route-level `pendingComponent` skeleton, shown only when no previous view can be retained (cold start, refresh, re-navigation after an error); the nearest matched ancestor's wins, and in-app navigation keeps the previous view instead
|
|
85
85
|
- Keeping the previous view during in-app navigation is an intentional design following browser-native semantics — see the Design Principles section of the core repository's README
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-routes.cjs","names":[],"sources":["../src/create-routes.
|
|
1
|
+
{"version":3,"file":"create-routes.cjs","names":[],"sources":["../src/create-routes.ts"],"sourcesContent":["import type {Route, SearchRoutesOf} from '@@/types';\n\n/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * The return type additionally closes the search loop(see\n * {@link SearchRoutesOf}): every level's `data` loader and `beforeLoad`\n * guard receive their `ctx.search` typed from the level's own\n * {@link Route.search search schema} —\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {\n * path: '/list',\n * search: z.object({page: z.coerce.number()}),\n * // typeof routes → ctx.search: {page: number}, no annotations\n * data: ({search}) => fetchList(search.page)\n * }\n * ]\n * });\n * ```\n *\n * Callbacks written inside the literal are still checked loosely\n * against `Route`(`ctx.search: any` — TypeScript cannot contextually\n * type a member from sibling properties); the precise types hold on the\n * returned table, and a callback whose annotation contradicts the\n * schema is rejected at the property. An explicit `Route<P, S>` generic\n * keeps priority wherever it is written.\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved and the\n * loader/guard search contexts re-typed from the schemas\n */\nexport function createRoutes<const T>(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead. The\n // `SearchRoutesOf<T>` member re-types the level contexts on the\n // checked argument, so an annotation that contradicts the level's\n // schema fails right at the property.\n routes: T & SearchRoutesOf<T> & (Route | Route[])\n): SearchRoutesOf<T> {\n return routes;\n}\n"],"mappings":"qBA0CA,SAQE,GAEA,OAAO,CACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-routes.js","names":[],"sources":["../src/create-routes.
|
|
1
|
+
{"version":3,"file":"create-routes.js","names":[],"sources":["../src/create-routes.ts"],"sourcesContent":["import type {Route, SearchRoutesOf} from '@@/types';\n\n/**\n * Identity function with `satisfies` semantics: the table is checked\n * against `Route` while every `path` keeps its string-literal type, so\n * `RoutePaths<typeof routes>` can extract the full pattern union for\n * `TypedLink`. An `as Route` assertion does the opposite — it widens\n * every `path` to `string` and gives up the literals.\n *\n * The return type additionally closes the search loop(see\n * {@link SearchRoutesOf}): every level's `data` loader and `beforeLoad`\n * guard receive their `ctx.search` typed from the level's own\n * {@link Route.search search schema} —\n *\n * ```tsx\n * const routes = createRoutes({\n * children: [\n * {\n * path: '/list',\n * search: z.object({page: z.coerce.number()}),\n * // typeof routes → ctx.search: {page: number}, no annotations\n * data: ({search}) => fetchList(search.page)\n * }\n * ]\n * });\n * ```\n *\n * Callbacks written inside the literal are still checked loosely\n * against `Route`(`ctx.search: any` — TypeScript cannot contextually\n * type a member from sibling properties); the precise types hold on the\n * returned table, and a callback whose annotation contradicts the\n * schema is rejected at the property. An explicit `Route<P, S>` generic\n * keeps priority wherever it is written.\n *\n * Zero runtime cost: the function returns its argument unchanged and\n * tree-shakes away.\n * @group Methods\n * @category Route\n * @param routes the route table, a route object or an array of them\n * @returns the very same route table, literal types preserved and the\n * loader/guard search contexts re-typed from the schemas\n */\nexport function createRoutes<const T>(\n // The `const` modifier keeps every `path` a string literal(through\n // arbitrary nesting) while the parameter is still checked against\n // `Route` — the `satisfies` semantics an `as Route` assertion lacks:\n // the assertion widens every `path` to `string` instead. The\n // `SearchRoutesOf<T>` member re-types the level contexts on the\n // checked argument, so an annotation that contradicts the level's\n // schema fails right at the property.\n routes: T & SearchRoutesOf<T> & (Route | Route[])\n): SearchRoutesOf<T> {\n return routes;\n}\n"],"mappings":"AA0CA,SAAgB,EAQd,GAEA,OAAO,CACT"}
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import type { BlockerFn } from '@native-router/core';
|
|
2
|
+
/**
|
|
3
|
+
* A vetoed navigation waiting for a decision, exposed on
|
|
4
|
+
* {@link Blocker.state}: the user was asked, the router stayed on
|
|
5
|
+
* {@link BlockerState.from from}.
|
|
6
|
+
*/
|
|
7
|
+
export type BlockerState = {
|
|
8
|
+
/** The vetoed navigation's target path (pathname, search, hash). */
|
|
9
|
+
location: string;
|
|
10
|
+
/** The path the vetoed navigation tried to leave. */
|
|
11
|
+
from: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* What {@link useBlocker} returns: whether a navigation is waiting for
|
|
15
|
+
* a decision, plus the decision channel. Both actions are no-ops while
|
|
16
|
+
* `state` is `null`.
|
|
17
|
+
*/
|
|
18
|
+
export type Blocker = {
|
|
19
|
+
/** The pending ask, or `null` while nothing waits — drive your confirm UI off it. */
|
|
20
|
+
state: BlockerState | null;
|
|
21
|
+
/**
|
|
22
|
+
* Retry the vetoed navigation. Only this hook's own blocker is
|
|
23
|
+
* bypassed — the user has answered it — while other registered
|
|
24
|
+
* blockers and the guard chain still get asked on the retry. A
|
|
25
|
+
* blocked-by-another-blocker retry re-enters this hook as a fresh
|
|
26
|
+
* veto and re-opens the ask.
|
|
27
|
+
*/
|
|
28
|
+
proceed(): void;
|
|
29
|
+
/** Dismiss the ask; the router stays where it is. */
|
|
30
|
+
reset(): void;
|
|
31
|
+
};
|
|
2
32
|
/**
|
|
3
33
|
* Block navigations away from the current page while the component is
|
|
4
34
|
* mounted — the unsaved-changes guard.
|
|
@@ -17,9 +47,36 @@ import type { BlockerFn } from '@native-router/core';
|
|
|
17
47
|
* here touches `window`, and the registration itself is an effect that
|
|
18
48
|
* never runs on the server.
|
|
19
49
|
*
|
|
50
|
+
* Every veto is tracked on the returned {@link Blocker}, so the
|
|
51
|
+
* confirm UI is a three-liner instead of a hand-rolled ref/state pair:
|
|
52
|
+
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* const blocker = useBlocker(() => isDirtyRef.current);
|
|
55
|
+
*
|
|
56
|
+
* return (
|
|
57
|
+
* <>
|
|
58
|
+
* <Editor />
|
|
59
|
+
* <ConfirmDialog
|
|
60
|
+
* open={blocker.state != null}
|
|
61
|
+
* onCancel={blocker.reset}
|
|
62
|
+
* onConfirm={blocker.proceed}
|
|
63
|
+
* />
|
|
64
|
+
* </>
|
|
65
|
+
* );
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* `proceed()` retries the vetoed navigation bypassing this hook's own
|
|
69
|
+
* blocker only — other registered blockers (and the route guards) are
|
|
70
|
+
* still asked, in registration order. Note the retry is a fresh push
|
|
71
|
+
* navigation: for a vetoed browser POP it appends an entry rather than
|
|
72
|
+
* re-running the history traversal.
|
|
73
|
+
*
|
|
20
74
|
* @group Hooks
|
|
21
75
|
* @param fn blocker predicate; `to` is the target path, `from` the
|
|
22
|
-
* current path
|
|
76
|
+
* current path. Return `false` to veto (and open the ask), `true` to
|
|
77
|
+
* let the navigation through
|
|
78
|
+
* @returns {@link Blocker} — the pending ask and its proceed/reset
|
|
79
|
+
* channel
|
|
23
80
|
* @see {@link setBlocker}
|
|
24
81
|
*/
|
|
25
|
-
export declare function useBlocker(fn: BlockerFn):
|
|
82
|
+
export declare function useBlocker(fn: BlockerFn): Blocker;
|
package/dist/use-blocker.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./components/Router.cjs");let r=require("react"),t=require("@native-router/core");exports.useBlocker=function(u){const c=e.useRouter(),
|
|
1
|
+
const e=require("./components/Router.cjs");let r=require("react"),t=require("@native-router/core");exports.useBlocker=function(u){const c=e.useRouter(),n=(0,r.useRef)(u);n.current=u;const[o,l]=(0,r.useState)(null),s=(0,r.useRef)(null),a=(0,r.useRef)(!1);return(0,r.useEffect)(()=>(0,t.setBlocker)(c,(e,r)=>a.current?(a.current=!1,!0):!!n.current(e,r)||(s.current={location:e,from:r},l({location:e,from:r}),!1)),[c]),{state:o,proceed:(0,r.useCallback)(()=>{const e=s.current;e&&(s.current=null,l(null),a.current=!0,(0,t.navigate)(c,e.location).catch(()=>{}))},[c]),reset:(0,r.useCallback)(()=>{s.current=null,l(null)},[])}};
|
|
2
2
|
//# sourceMappingURL=use-blocker.cjs.map
|
package/dist/use-blocker.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-blocker.cjs","names":[],"sources":["../src/use-blocker.ts"],"sourcesContent":["import {useEffect, useRef} from 'react';\nimport {setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn):
|
|
1
|
+
{"version":3,"file":"use-blocker.cjs","names":[],"sources":["../src/use-blocker.ts"],"sourcesContent":["import {useCallback, useEffect, useRef, useState} from 'react';\nimport {navigate, setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * A vetoed navigation waiting for a decision, exposed on\n * {@link Blocker.state}: the user was asked, the router stayed on\n * {@link BlockerState.from from}.\n */\nexport type BlockerState = {\n /** The vetoed navigation's target path (pathname, search, hash). */\n location: string;\n /** The path the vetoed navigation tried to leave. */\n from: string;\n};\n\n/**\n * What {@link useBlocker} returns: whether a navigation is waiting for\n * a decision, plus the decision channel. Both actions are no-ops while\n * `state` is `null`.\n */\nexport type Blocker = {\n /** The pending ask, or `null` while nothing waits — drive your confirm UI off it. */\n state: BlockerState | null;\n /**\n * Retry the vetoed navigation. Only this hook's own blocker is\n * bypassed — the user has answered it — while other registered\n * blockers and the guard chain still get asked on the retry. A\n * blocked-by-another-blocker retry re-enters this hook as a fresh\n * veto and re-opens the ask.\n */\n proceed(): void;\n /** Dismiss the ask; the router stays where it is. */\n reset(): void;\n};\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * Every veto is tracked on the returned {@link Blocker}, so the\n * confirm UI is a three-liner instead of a hand-rolled ref/state pair:\n *\n * ```tsx\n * const blocker = useBlocker(() => isDirtyRef.current);\n *\n * return (\n * <>\n * <Editor />\n * <ConfirmDialog\n * open={blocker.state != null}\n * onCancel={blocker.reset}\n * onConfirm={blocker.proceed}\n * />\n * </>\n * );\n * ```\n *\n * `proceed()` retries the vetoed navigation bypassing this hook's own\n * blocker only — other registered blockers (and the route guards) are\n * still asked, in registration order. Note the retry is a fresh push\n * navigation: for a vetoed browser POP it appends an entry rather than\n * re-running the history traversal.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path. Return `false` to veto (and open the ask), `true` to\n * let the navigation through\n * @returns {@link Blocker} — the pending ask and its proceed/reset\n * channel\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn): Blocker {\n const router = useRouter();\n const fnRef = useRef(fn);\n // Always ask the latest closure: re-rendering with new state must not\n // require re-registering the blocker.\n fnRef.current = fn;\n const [ask, setAsk] = useState<BlockerState | null>(null);\n // The pending ask, readable synchronously from proceed/reset — the\n // confirm callbacks must not depend on the render closure's freshness.\n const pendingRef = useRef<BlockerState | null>(null);\n // One-shot bypass for the proceed retry: set right before the retry\n // navigation, cleared once it has been asked. A plain ref — the flag\n // must be visible to the synchronously-asked blocker, not to React.\n const bypassRef = useRef(false);\n\n useEffect(\n () =>\n setBlocker(router, (to, from) => {\n // The proceed retry: the user already answered this blocker.\n // Still ask the other registered ones (setBlocker iterates the\n // registry in order) and reset the flag once we are asked.\n if (bypassRef.current) {\n bypassRef.current = false;\n return true;\n }\n if (fnRef.current(to, from)) return true;\n // Vetoed: open the ask. A navigation superseding an open ask\n // replaces it — the older target is never proceeded to.\n pendingRef.current = {location: to, from};\n setAsk({location: to, from});\n return false;\n }),\n [router]\n );\n\n const proceed = useCallback(() => {\n const pending = pendingRef.current;\n if (!pending) return;\n pendingRef.current = null;\n setAsk(null);\n bypassRef.current = true;\n void navigate(router, pending.location).catch(() => undefined);\n }, [router]);\n\n const reset = useCallback(() => {\n pendingRef.current = null;\n setAsk(null);\n }, []);\n\n return {state: ask, proceed, reset};\n}\n"],"mappings":"sHAuFA,SAA2B,GACzB,MAAM,EAAS,EAAA,YACT,GAAA,EAAQ,EAAA,QAAO,GAGrB,EAAM,QAAU,EAChB,MAAO,EAAK,IAAA,EAAU,EAAA,UAA8B,MAG9C,GAAA,EAAa,EAAA,QAA4B,MAIzC,GAAA,EAAY,EAAA,SAAO,GAoCzB,OAlCA,EAAA,EAAA,WAAA,KAAA,EAEI,EAAA,YAAW,EAAA,CAAS,EAAI,IAIlB,EAAU,SACZ,EAAU,SAAU,GACb,KAEL,EAAM,QAAQ,EAAI,KAGtB,EAAW,QAAU,CAAC,SAAU,EAAI,QACpC,EAAO,CAAC,SAAU,EAAI,UACf,IAEX,CAAC,IAiBI,CAAC,MAAO,EAAK,SAAA,EAdJ,EAAA,aAAA,KACd,MAAM,EAAU,EAAW,QACtB,IACL,EAAW,QAAU,KACrB,EAAO,MACP,EAAU,SAAU,GACpB,EAAK,EAAA,UAAS,EAAQ,EAAQ,UAAU,MAAA,UACvC,CAAC,IAOyB,OAAA,EALf,EAAA,aAAA,KACZ,EAAW,QAAU,KACrB,EAAO,OACN,IAGL"}
|
package/dist/use-blocker.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{useRouter as r}from"./components/Router.js";import{useEffect as
|
|
1
|
+
import{useRouter as r}from"./components/Router.js";import{useCallback as t,useEffect as n,useRef as o,useState as c}from"react";import{navigate as e,setBlocker as u}from"@native-router/core";function l(l){const m=r(),i=o(l);i.current=l;const[a,s]=c(null),f=o(null),p=o(!1);return n(()=>u(m,(r,t)=>p.current?(p.current=!1,!0):!!i.current(r,t)||(f.current={location:r,from:t},s({location:r,from:t}),!1)),[m]),{state:a,proceed:t(()=>{const r=f.current;r&&(f.current=null,s(null),p.current=!0,e(m,r.location).catch(()=>{}))},[m]),reset:t(()=>{f.current=null,s(null)},[])}}export{l as useBlocker};
|
|
2
2
|
//# sourceMappingURL=use-blocker.js.map
|
package/dist/use-blocker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-blocker.js","names":[],"sources":["../src/use-blocker.ts"],"sourcesContent":["import {useEffect, useRef} from 'react';\nimport {setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn):
|
|
1
|
+
{"version":3,"file":"use-blocker.js","names":[],"sources":["../src/use-blocker.ts"],"sourcesContent":["import {useCallback, useEffect, useRef, useState} from 'react';\nimport {navigate, setBlocker} from '@native-router/core';\nimport type {BlockerFn} from '@native-router/core';\nimport {useRouter} from './components/Router';\n\n/**\n * A vetoed navigation waiting for a decision, exposed on\n * {@link Blocker.state}: the user was asked, the router stayed on\n * {@link BlockerState.from from}.\n */\nexport type BlockerState = {\n /** The vetoed navigation's target path (pathname, search, hash). */\n location: string;\n /** The path the vetoed navigation tried to leave. */\n from: string;\n};\n\n/**\n * What {@link useBlocker} returns: whether a navigation is waiting for\n * a decision, plus the decision channel. Both actions are no-ops while\n * `state` is `null`.\n */\nexport type Blocker = {\n /** The pending ask, or `null` while nothing waits — drive your confirm UI off it. */\n state: BlockerState | null;\n /**\n * Retry the vetoed navigation. Only this hook's own blocker is\n * bypassed — the user has answered it — while other registered\n * blockers and the guard chain still get asked on the retry. A\n * blocked-by-another-blocker retry re-enters this hook as a fresh\n * veto and re-opens the ask.\n */\n proceed(): void;\n /** Dismiss the ask; the router stays where it is. */\n reset(): void;\n};\n\n/**\n * Block navigations away from the current page while the component is\n * mounted — the unsaved-changes guard.\n *\n * The predicate is the core `setBlocker` veto: `(to, from) => boolean`\n * over path strings(including search and hash), asked synchronously at\n * the head of every navigation and before a history POP lands. Return\n * `false` to veto: a vetoed navigation never starts and a vetoed POP is\n * rewound. `refresh` and guard redirects are never blocked; the effect\n * releases the blocker on unmount, so the guard lives exactly as long\n * as the guarding component.\n *\n * The predicate is stored in a ref and re-synced on every render, so a\n * navigation is always asked the latest closure — a `confirmed` flag it\n * captured works without re-registering anything. SSR-safe: nothing\n * here touches `window`, and the registration itself is an effect that\n * never runs on the server.\n *\n * Every veto is tracked on the returned {@link Blocker}, so the\n * confirm UI is a three-liner instead of a hand-rolled ref/state pair:\n *\n * ```tsx\n * const blocker = useBlocker(() => isDirtyRef.current);\n *\n * return (\n * <>\n * <Editor />\n * <ConfirmDialog\n * open={blocker.state != null}\n * onCancel={blocker.reset}\n * onConfirm={blocker.proceed}\n * />\n * </>\n * );\n * ```\n *\n * `proceed()` retries the vetoed navigation bypassing this hook's own\n * blocker only — other registered blockers (and the route guards) are\n * still asked, in registration order. Note the retry is a fresh push\n * navigation: for a vetoed browser POP it appends an entry rather than\n * re-running the history traversal.\n *\n * @group Hooks\n * @param fn blocker predicate; `to` is the target path, `from` the\n * current path. Return `false` to veto (and open the ask), `true` to\n * let the navigation through\n * @returns {@link Blocker} — the pending ask and its proceed/reset\n * channel\n * @see {@link setBlocker}\n */\nexport function useBlocker(fn: BlockerFn): Blocker {\n const router = useRouter();\n const fnRef = useRef(fn);\n // Always ask the latest closure: re-rendering with new state must not\n // require re-registering the blocker.\n fnRef.current = fn;\n const [ask, setAsk] = useState<BlockerState | null>(null);\n // The pending ask, readable synchronously from proceed/reset — the\n // confirm callbacks must not depend on the render closure's freshness.\n const pendingRef = useRef<BlockerState | null>(null);\n // One-shot bypass for the proceed retry: set right before the retry\n // navigation, cleared once it has been asked. A plain ref — the flag\n // must be visible to the synchronously-asked blocker, not to React.\n const bypassRef = useRef(false);\n\n useEffect(\n () =>\n setBlocker(router, (to, from) => {\n // The proceed retry: the user already answered this blocker.\n // Still ask the other registered ones (setBlocker iterates the\n // registry in order) and reset the flag once we are asked.\n if (bypassRef.current) {\n bypassRef.current = false;\n return true;\n }\n if (fnRef.current(to, from)) return true;\n // Vetoed: open the ask. A navigation superseding an open ask\n // replaces it — the older target is never proceeded to.\n pendingRef.current = {location: to, from};\n setAsk({location: to, from});\n return false;\n }),\n [router]\n );\n\n const proceed = useCallback(() => {\n const pending = pendingRef.current;\n if (!pending) return;\n pendingRef.current = null;\n setAsk(null);\n bypassRef.current = true;\n void navigate(router, pending.location).catch(() => undefined);\n }, [router]);\n\n const reset = useCallback(() => {\n pendingRef.current = null;\n setAsk(null);\n }, []);\n\n return {state: ask, proceed, reset};\n}\n"],"mappings":"+LAuFA,SAAgB,EAAW,GACzB,MAAM,EAAS,IACT,EAAQ,EAAO,GAGrB,EAAM,QAAU,EAChB,MAAO,EAAK,GAAU,EAA8B,MAG9C,EAAa,EAA4B,MAIzC,EAAY,GAAO,GAoCzB,OAlCA,EAAA,IAEI,EAAW,EAAA,CAAS,EAAI,IAIlB,EAAU,SACZ,EAAU,SAAU,GACb,KAEL,EAAM,QAAQ,EAAI,KAGtB,EAAW,QAAU,CAAC,SAAU,EAAI,QACpC,EAAO,CAAC,SAAU,EAAI,UACf,IAEX,CAAC,IAiBI,CAAC,MAAO,EAAK,QAdJ,EAAA,KACd,MAAM,EAAU,EAAW,QACtB,IACL,EAAW,QAAU,KACrB,EAAO,MACP,EAAU,SAAU,EACpB,EAAc,EAAQ,EAAQ,UAAU,MAAA,UACvC,CAAC,IAOyB,MALf,EAAA,KACZ,EAAW,QAAU,KACrB,EAAO,OACN,IAGL"}
|