@native-router/react 1.7.0 → 1.7.1

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.
@@ -67,9 +67,11 @@ export type Blocker = {
67
67
  *
68
68
  * `proceed()` retries the vetoed navigation bypassing this hook's own
69
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.
70
+ * still asked, in registration order. The bypass is strictly one-shot:
71
+ * whether the retry lands or is vetoed by another blocker, it never
72
+ * carries over to a later, unrelated navigation. Note the retry is a
73
+ * fresh push navigation: for a vetoed browser POP it appends an entry
74
+ * rather than re-running the history traversal.
73
75
  *
74
76
  * @group Hooks
75
77
  * @param fn blocker predicate; `to` is the target path, `from` the
@@ -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(),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)},[])}};
1
+ const e=require("./components/Router.cjs");let r=require("react"),t=require("@native-router/core");exports.useBlocker=function(u){const n=e.useRouter(),c=(0,r.useRef)(u);c.current=u;const[l,o]=(0,r.useState)(null),s=(0,r.useRef)(null),a=(0,r.useRef)(!1);return(0,r.useEffect)(()=>(0,t.setBlocker)(n,(e,r)=>a.current?(a.current=!1,!0):!!c.current(e,r)||(s.current={location:e,from:r},o({location:e,from:r}),!1)),[n]),{state:l,proceed:(0,r.useCallback)(()=>{const e=s.current;if(e){s.current=null,o(null),a.current=!0;try{(0,t.navigate)(n,e.location).catch(()=>{})}finally{a.current=!1}}},[n]),reset:(0,r.useCallback)(()=>{s.current=null,o(null)},[])}};
2
2
  //# sourceMappingURL=use-blocker.cjs.map
@@ -1 +1 @@
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"}
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. The bypass is strictly one-shot:\n * whether the retry lands or is vetoed by another blocker, it never\n * carries over to a later, unrelated navigation. Note the retry is a\n * fresh push navigation: for a vetoed browser POP it appends an entry\n * rather than 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 the retry's blocker asking has run asked\n // and consumed, or truncated by an earlier blocker's veto(see\n // `proceed`). A plain ref — the flag must be visible to the\n // 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 // `navigate` asks the blockers synchronously at its head, so by the\n // time the call returns the one-shot flag is either consumed(this\n // blocker was asked and let the retry through) or the retry was\n // truncated by an earlier-registered blocker's veto. Both outcomes\n // must extinguish the flag: leaving it set would silently bypass\n // this guard on the next, unrelated navigation.\n try {\n void navigate(router, pending.location).catch(() => undefined);\n } finally {\n bypassRef.current = false;\n }\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":"sHAyFA,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,MAMzC,GAAA,EAAY,EAAA,SAAO,GA8CzB,OA5CA,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,IA2BI,CAAC,MAAO,EAAK,SAAA,EAxBJ,EAAA,aAAA,KACd,MAAM,EAAU,EAAW,QAC3B,GAAK,EAAL,CACA,EAAW,QAAU,KACrB,EAAO,MACP,EAAU,SAAU,EAOpB,KACE,EAAK,EAAA,UAAS,EAAQ,EAAQ,UAAU,MAAA,OAC1C,CAAA,QACE,EAAU,SAAU,CACtB,CAdc,GAeb,CAAC,IAOyB,OAAA,EALf,EAAA,aAAA,KACZ,EAAW,QAAU,KACrB,EAAO,OACN,IAGL"}
@@ -1,2 +1,2 @@
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};
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 i=r(),m=o(l);m.current=l;const[a,f]=c(null),s=o(null),p=o(!1);return n(()=>u(i,(r,t)=>p.current?(p.current=!1,!0):!!m.current(r,t)||(s.current={location:r,from:t},f({location:r,from:t}),!1)),[i]),{state:a,proceed:t(()=>{const r=s.current;if(r){s.current=null,f(null),p.current=!0;try{e(i,r.location).catch(()=>{})}finally{p.current=!1}}},[i]),reset:t(()=>{s.current=null,f(null)},[])}}export{l as useBlocker};
2
2
  //# sourceMappingURL=use-blocker.js.map
@@ -1 +1 @@
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"}
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. The bypass is strictly one-shot:\n * whether the retry lands or is vetoed by another blocker, it never\n * carries over to a later, unrelated navigation. Note the retry is a\n * fresh push navigation: for a vetoed browser POP it appends an entry\n * rather than 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 the retry's blocker asking has run asked\n // and consumed, or truncated by an earlier blocker's veto(see\n // `proceed`). A plain ref — the flag must be visible to the\n // 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 // `navigate` asks the blockers synchronously at its head, so by the\n // time the call returns the one-shot flag is either consumed(this\n // blocker was asked and let the retry through) or the retry was\n // truncated by an earlier-registered blocker's veto. Both outcomes\n // must extinguish the flag: leaving it set would silently bypass\n // this guard on the next, unrelated navigation.\n try {\n void navigate(router, pending.location).catch(() => undefined);\n } finally {\n bypassRef.current = false;\n }\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":"+LAyFA,SAAgB,EAAW,GACzB,MAAM,EAAS,IACT,EAAQ,EAAO,GAGrB,EAAM,QAAU,EAChB,MAAO,EAAK,GAAU,EAA8B,MAG9C,EAAa,EAA4B,MAMzC,EAAY,GAAO,GA8CzB,OA5CA,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,IA2BI,CAAC,MAAO,EAAK,QAxBJ,EAAA,KACd,MAAM,EAAU,EAAW,QAC3B,GAAK,EAAL,CACA,EAAW,QAAU,KACrB,EAAO,MACP,EAAU,SAAU,EAOpB,IACE,EAAc,EAAQ,EAAQ,UAAU,MAAA,OAC1C,CAAA,QACE,EAAU,SAAU,CACtB,CAdc,GAeb,CAAC,IAOyB,MALf,EAAA,KACZ,EAAW,QAAU,KACrB,EAAO,OACN,IAGL"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-router/react",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {