@rshono/core 1.0.0-rc.26 → 1.0.0-rc.27

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.
@@ -82,7 +82,11 @@ export interface AsyncBoundaryProps {
82
82
  error?: ErrorFallback;
83
83
  /** Called with the caught error. From a `'use client'` component only — see {@link CatchBoundaryProps.onError}. */
84
84
  onError?: (error: Error) => void;
85
- /** Clears the error fallback when any value changes — see {@link CatchBoundaryProps.resetKeys}. */
85
+ /**
86
+ * Clears the error fallback when any value changes — see {@link CatchBoundaryProps.resetKeys}. A
87
+ * pathname change already clears the whole boundary — see {@link AsyncBoundary} — so this is for
88
+ * resetting on anything else, an id or a filter say.
89
+ */
86
90
  resetKeys?: readonly unknown[];
87
91
  /** The subtree this boundary suspends on and protects. */
88
92
  children: ReactNode;
@@ -100,6 +104,15 @@ export interface AsyncBoundaryProps {
100
104
  * so `loading` shows until the children resolve and `error` catches whatever they throw, suspended or
101
105
  * not. `error` is optional: omit it and errors propagate to the next boundary out.
102
106
  *
107
+ * The boundary is scoped to the route: it is keyed to the current pathname, so a soft navigation to a
108
+ * different route mounts the incoming route's boundary and its `loading` fallback shows while its
109
+ * children stream. Without that, React treats the incoming page as a transition onto an already-revealed
110
+ * boundary and keeps the outgoing route's content on screen instead of the fallback. A same-route update
111
+ * — `router.refresh()`, a server action, a query-string change — keeps the boundary and its revealed
112
+ * content, which is what makes those updates seamless. A section whose state is meant to outlive a route
113
+ * change wants a bare {@link CatchBoundary} (with an ancestor's `Suspense`, or none) rather than an
114
+ * `AsyncBoundary`.
115
+ *
103
116
  * @example
104
117
  * ```tsx
105
118
  * import { AsyncBoundary } from '@rshono/core/client';
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { Component, Suspense } from 'react';
4
4
  import { isControlDigest } from './control.js';
5
+ import { useNavigationPathname } from './navigation.js';
5
6
  // `redirect()` and `notFound()` reach the browser as a thrown error carrying a control digest. They are
6
7
  // navigation, not failure, so no boundary absorbs one — they are re-thrown to the root, where the
7
8
  // runtime turns the digest into a real navigation.
@@ -81,6 +82,15 @@ export class CatchBoundary extends Component {
81
82
  * so `loading` shows until the children resolve and `error` catches whatever they throw, suspended or
82
83
  * not. `error` is optional: omit it and errors propagate to the next boundary out.
83
84
  *
85
+ * The boundary is scoped to the route: it is keyed to the current pathname, so a soft navigation to a
86
+ * different route mounts the incoming route's boundary and its `loading` fallback shows while its
87
+ * children stream. Without that, React treats the incoming page as a transition onto an already-revealed
88
+ * boundary and keeps the outgoing route's content on screen instead of the fallback. A same-route update
89
+ * — `router.refresh()`, a server action, a query-string change — keeps the boundary and its revealed
90
+ * content, which is what makes those updates seamless. A section whose state is meant to outlive a route
91
+ * change wants a bare {@link CatchBoundary} (with an ancestor's `Suspense`, or none) rather than an
92
+ * `AsyncBoundary`.
93
+ *
84
94
  * @example
85
95
  * ```tsx
86
96
  * import { AsyncBoundary } from '@rshono/core/client';
@@ -94,6 +104,10 @@ export class CatchBoundary extends Component {
94
104
  * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}
95
105
  */
96
106
  export function AsyncBoundary({ loading, error, onError, resetKeys, children }) {
97
- return (_jsx(CatchBoundary, { fallback: error, onError: onError, resetKeys: resetKeys, children: _jsx(Suspense, { fallback: loading, children: children }) }));
107
+ // The pathname as the key is what makes a navigation mount this boundary rather than reconcile into
108
+ // the one the outgoing route revealed — see the component's docs. `undefined` outside a page's tree
109
+ // keys nothing, which keeps a boundary rendered somewhere else behaving as it always has.
110
+ const pathname = useNavigationPathname();
111
+ return (_jsx(CatchBoundary, { fallback: error, onError: onError, resetKeys: resetKeys, children: _jsx(Suspense, { fallback: loading, children: children }) }, pathname));
98
112
  }
99
113
  //# sourceMappingURL=boundaries.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"boundaries.js","sourceRoot":"","sources":["../../src/runtime/boundaries.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,wGAAwG;AACxG,kGAAkG;AAClG,mDAAmD;AACnD,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,eAAe,CAAE,KAAqC,EAAE,MAAM,CAAC,CAAC;AACzE,CAAC;AA2CD,SAAS,WAAW,CAAC,CAAqB,EAAE,CAAqB;IAC/D,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAiD;IAClF,KAAK,GAAuB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,iBAAiB,CAAC,KAAY;QAC5B,IAAI,cAAc,CAAC,KAAK,CAAC;YAAE,OAAO;QAClC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,kBAAkB,CAAC,IAAwB;QACzC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QACjC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,IAAI,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;YAC9F,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,GAAG,GAAS,EAAE;QACjB,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,CAAC,CAAC;IAEF,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC;YACvC,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YAChC,IAAI,QAAQ,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC,CAAC,iCAAiC;YAC1E,OAAO,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACjF,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC7B,CAAC;CACF;AAoBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAsB;IAChG,OAAO,CACL,KAAC,aAAa,IAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,YACpE,KAAC,QAAQ,IAAC,QAAQ,EAAE,OAAO,YAAG,QAAQ,GAAY,GACpC,CACjB,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Component, Suspense, type ReactNode } from 'react';\nimport { isControlDigest } from './control.js';\n\n// `redirect()` and `notFound()` reach the browser as a thrown error carrying a control digest. They are\n// navigation, not failure, so no boundary absorbs one — they are re-thrown to the root, where the\n// runtime turns the digest into a real navigation.\nfunction isControlError(error: unknown): boolean {\n return isControlDigest((error as { digest?: unknown } | null)?.digest);\n}\n\n/**\n * What a {@link CatchBoundary} / {@link AsyncBoundary} renders once a child throws: either a static\n * node, or a render function given the error and a `reset` callback that clears it and re-renders the\n * children (a \"Try again\" button, say).\n *\n * The function form only works from a `'use client'` component — functions can't cross the\n * server→client boundary. From a server component, pass a `ReactNode`.\n */\nexport type ErrorFallback = ReactNode | ((error: Error, reset: () => void) => ReactNode);\n\n/** Props for {@link CatchBoundary}. */\nexport interface CatchBoundaryProps {\n /**\n * Rendered in place of the children after one of them throws. Omit it to report the error via\n * `onError` and re-throw to the next boundary out — or the app's `error` page — instead of handling it\n * here.\n */\n fallback?: ErrorFallback;\n /**\n * Called with the caught error, for logging or reporting.\n *\n * A function prop, so — like {@link ErrorFallback}'s function form — it can only be passed from a\n * `'use client'` component. React refuses one from a server component by name: \"Event handlers cannot be\n * passed to Client Component props\".\n */\n onError?: (error: Error) => void;\n /**\n * Clears the error automatically when any value in this array changes while the fallback is showing.\n * Pass the current pathname to recover when the user navigates away — `resetKeys={[url.pathname]}` from a\n * page's `url` prop, which is the form that works from the server component rendering this boundary, or\n * `resetKeys={[useNavigation().url.pathname]}` inside a `'use client'` component.\n */\n resetKeys?: readonly unknown[];\n /** The subtree this boundary protects. */\n children: ReactNode;\n}\n\ninterface CatchBoundaryState {\n error: Error | null;\n}\n\nfunction keysChanged(a: readonly unknown[], b: readonly unknown[]): boolean {\n return a.length !== b.length || a.some((value, i) => !Object.is(value, b[i]));\n}\n\n/**\n * A general-purpose error boundary: catches what its children throw — a client island that blew up, a\n * server component that rejected on a soft navigation — and renders `fallback` in their place rather\n * than tearing down the page.\n *\n * It is a `'use client'` component (React error boundaries must be), so a server component can render\n * it too. Reach for {@link AsyncBoundary} when you also want a Suspense loading fallback.\n *\n * @example\n * ```tsx\n * import { CatchBoundary } from '@rshono/core/client';\n *\n * <CatchBoundary fallback={(error, reset) => (\n * <div role=\"alert\">\n * <p>{error.message}</p>\n * <button onClick={reset}>Try again</button>\n * </div>\n * )}>\n * <RiskyWidget />\n * </CatchBoundary>\n * ```\n *\n * @see {@link https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary | React — error boundaries}\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n */\nexport class CatchBoundary extends Component<CatchBoundaryProps, CatchBoundaryState> {\n state: CatchBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CatchBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error): void {\n if (isControlError(error)) return;\n this.props.onError?.(error);\n }\n\n componentDidUpdate(prev: CatchBoundaryProps): void {\n const { resetKeys } = this.props;\n if (this.state.error && prev.resetKeys && resetKeys && keysChanged(prev.resetKeys, resetKeys)) {\n this.reset();\n }\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (error !== null) {\n if (isControlError(error)) throw error;\n const { fallback } = this.props;\n if (fallback === undefined) throw error; // propagate to an outer boundary\n return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;\n }\n return this.props.children;\n }\n}\n\n/** Props for {@link AsyncBoundary}. */\nexport interface AsyncBoundaryProps {\n /**\n * Suspense fallback, shown while the children or their data are still loading. Required — a loading\n * state is the reason to reach for this over {@link CatchBoundary}, so showing nothing is an explicit\n * `loading={null}`.\n */\n loading: ReactNode;\n /** Error fallback, shown if a child throws. See {@link ErrorFallback}. */\n error?: ErrorFallback;\n /** Called with the caught error. From a `'use client'` component only — see {@link CatchBoundaryProps.onError}. */\n onError?: (error: Error) => void;\n /** Clears the error fallback when any value changes — see {@link CatchBoundaryProps.resetKeys}. */\n resetKeys?: readonly unknown[];\n /** The subtree this boundary suspends on and protects. */\n children: ReactNode;\n}\n\n/**\n * A loading and error boundary in one wrapper — the common case for an async section of a page. It\n * always renders the same shape:\n *\n * ```tsx\n * <CatchBoundary fallback={error}>\n * <Suspense fallback={loading}>{children}</Suspense>\n * </CatchBoundary>\n * ```\n *\n * so `loading` shows until the children resolve and `error` catches whatever they throw, suspended or\n * not. `error` is optional: omit it and errors propagate to the next boundary out.\n *\n * @example\n * ```tsx\n * import { AsyncBoundary } from '@rshono/core/client';\n *\n * <AsyncBoundary loading={<Spinner />} error={(e, reset) => <Retry onClick={reset} />}>\n * <SlowServerComponent />\n * </AsyncBoundary>\n * ```\n *\n * @see {@link https://react.dev/reference/react/Suspense | React — `<Suspense>`}\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n */\nexport function AsyncBoundary({ loading, error, onError, resetKeys, children }: AsyncBoundaryProps): ReactNode {\n return (\n <CatchBoundary fallback={error} onError={onError} resetKeys={resetKeys}>\n <Suspense fallback={loading}>{children}</Suspense>\n </CatchBoundary>\n );\n}\n"]}
1
+ {"version":3,"file":"boundaries.js","sourceRoot":"","sources":["../../src/runtime/boundaries.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAkB,MAAM,OAAO,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAExD,wGAAwG;AACxG,kGAAkG;AAClG,mDAAmD;AACnD,SAAS,cAAc,CAAC,KAAc;IACpC,OAAO,eAAe,CAAE,KAAqC,EAAE,MAAM,CAAC,CAAC;AACzE,CAAC;AA2CD,SAAS,WAAW,CAAC,CAAqB,EAAE,CAAqB;IAC/D,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,OAAO,aAAc,SAAQ,SAAiD;IAClF,KAAK,GAAuB,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE5C,MAAM,CAAC,wBAAwB,CAAC,KAAY;QAC1C,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IAED,iBAAiB,CAAC,KAAY;QAC5B,IAAI,cAAc,CAAC,KAAK,CAAC;YAAE,OAAO;QAClC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAED,kBAAkB,CAAC,IAAwB;QACzC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QACjC,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,SAAS,IAAI,SAAS,IAAI,WAAW,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC;YAC9F,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,GAAG,GAAS,EAAE;QACjB,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACjC,CAAC,CAAC;IAEF,MAAM;QACJ,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;QAC7B,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,IAAI,cAAc,CAAC,KAAK,CAAC;gBAAE,MAAM,KAAK,CAAC;YACvC,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YAChC,IAAI,QAAQ,KAAK,SAAS;gBAAE,MAAM,KAAK,CAAC,CAAC,iCAAiC;YAC1E,OAAO,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACjF,CAAC;QACD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC;IAC7B,CAAC;CACF;AAwBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAsB;IAChG,oGAAoG;IACpG,oGAAoG;IACpG,0FAA0F;IAC1F,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAC;IACzC,OAAO,CACL,KAAC,aAAa,IAAgB,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,YACnF,KAAC,QAAQ,IAAC,QAAQ,EAAE,OAAO,YAAG,QAAQ,GAAY,IADhC,QAAQ,CAEZ,CACjB,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\nimport { Component, Suspense, type ReactNode } from 'react';\nimport { isControlDigest } from './control.js';\nimport { useNavigationPathname } from './navigation.js';\n\n// `redirect()` and `notFound()` reach the browser as a thrown error carrying a control digest. They are\n// navigation, not failure, so no boundary absorbs one — they are re-thrown to the root, where the\n// runtime turns the digest into a real navigation.\nfunction isControlError(error: unknown): boolean {\n return isControlDigest((error as { digest?: unknown } | null)?.digest);\n}\n\n/**\n * What a {@link CatchBoundary} / {@link AsyncBoundary} renders once a child throws: either a static\n * node, or a render function given the error and a `reset` callback that clears it and re-renders the\n * children (a \"Try again\" button, say).\n *\n * The function form only works from a `'use client'` component — functions can't cross the\n * server→client boundary. From a server component, pass a `ReactNode`.\n */\nexport type ErrorFallback = ReactNode | ((error: Error, reset: () => void) => ReactNode);\n\n/** Props for {@link CatchBoundary}. */\nexport interface CatchBoundaryProps {\n /**\n * Rendered in place of the children after one of them throws. Omit it to report the error via\n * `onError` and re-throw to the next boundary out — or the app's `error` page — instead of handling it\n * here.\n */\n fallback?: ErrorFallback;\n /**\n * Called with the caught error, for logging or reporting.\n *\n * A function prop, so — like {@link ErrorFallback}'s function form — it can only be passed from a\n * `'use client'` component. React refuses one from a server component by name: \"Event handlers cannot be\n * passed to Client Component props\".\n */\n onError?: (error: Error) => void;\n /**\n * Clears the error automatically when any value in this array changes while the fallback is showing.\n * Pass the current pathname to recover when the user navigates away — `resetKeys={[url.pathname]}` from a\n * page's `url` prop, which is the form that works from the server component rendering this boundary, or\n * `resetKeys={[useNavigation().url.pathname]}` inside a `'use client'` component.\n */\n resetKeys?: readonly unknown[];\n /** The subtree this boundary protects. */\n children: ReactNode;\n}\n\ninterface CatchBoundaryState {\n error: Error | null;\n}\n\nfunction keysChanged(a: readonly unknown[], b: readonly unknown[]): boolean {\n return a.length !== b.length || a.some((value, i) => !Object.is(value, b[i]));\n}\n\n/**\n * A general-purpose error boundary: catches what its children throw — a client island that blew up, a\n * server component that rejected on a soft navigation — and renders `fallback` in their place rather\n * than tearing down the page.\n *\n * It is a `'use client'` component (React error boundaries must be), so a server component can render\n * it too. Reach for {@link AsyncBoundary} when you also want a Suspense loading fallback.\n *\n * @example\n * ```tsx\n * import { CatchBoundary } from '@rshono/core/client';\n *\n * <CatchBoundary fallback={(error, reset) => (\n * <div role=\"alert\">\n * <p>{error.message}</p>\n * <button onClick={reset}>Try again</button>\n * </div>\n * )}>\n * <RiskyWidget />\n * </CatchBoundary>\n * ```\n *\n * @see {@link https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary | React — error boundaries}\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n */\nexport class CatchBoundary extends Component<CatchBoundaryProps, CatchBoundaryState> {\n state: CatchBoundaryState = { error: null };\n\n static getDerivedStateFromError(error: Error): CatchBoundaryState {\n return { error };\n }\n\n componentDidCatch(error: Error): void {\n if (isControlError(error)) return;\n this.props.onError?.(error);\n }\n\n componentDidUpdate(prev: CatchBoundaryProps): void {\n const { resetKeys } = this.props;\n if (this.state.error && prev.resetKeys && resetKeys && keysChanged(prev.resetKeys, resetKeys)) {\n this.reset();\n }\n }\n\n reset = (): void => {\n this.setState({ error: null });\n };\n\n render(): ReactNode {\n const { error } = this.state;\n if (error !== null) {\n if (isControlError(error)) throw error;\n const { fallback } = this.props;\n if (fallback === undefined) throw error; // propagate to an outer boundary\n return typeof fallback === 'function' ? fallback(error, this.reset) : fallback;\n }\n return this.props.children;\n }\n}\n\n/** Props for {@link AsyncBoundary}. */\nexport interface AsyncBoundaryProps {\n /**\n * Suspense fallback, shown while the children or their data are still loading. Required — a loading\n * state is the reason to reach for this over {@link CatchBoundary}, so showing nothing is an explicit\n * `loading={null}`.\n */\n loading: ReactNode;\n /** Error fallback, shown if a child throws. See {@link ErrorFallback}. */\n error?: ErrorFallback;\n /** Called with the caught error. From a `'use client'` component only — see {@link CatchBoundaryProps.onError}. */\n onError?: (error: Error) => void;\n /**\n * Clears the error fallback when any value changes — see {@link CatchBoundaryProps.resetKeys}. A\n * pathname change already clears the whole boundary — see {@link AsyncBoundary} — so this is for\n * resetting on anything else, an id or a filter say.\n */\n resetKeys?: readonly unknown[];\n /** The subtree this boundary suspends on and protects. */\n children: ReactNode;\n}\n\n/**\n * A loading and error boundary in one wrapper — the common case for an async section of a page. It\n * always renders the same shape:\n *\n * ```tsx\n * <CatchBoundary fallback={error}>\n * <Suspense fallback={loading}>{children}</Suspense>\n * </CatchBoundary>\n * ```\n *\n * so `loading` shows until the children resolve and `error` catches whatever they throw, suspended or\n * not. `error` is optional: omit it and errors propagate to the next boundary out.\n *\n * The boundary is scoped to the route: it is keyed to the current pathname, so a soft navigation to a\n * different route mounts the incoming route's boundary and its `loading` fallback shows while its\n * children stream. Without that, React treats the incoming page as a transition onto an already-revealed\n * boundary and keeps the outgoing route's content on screen instead of the fallback. A same-route update\n * — `router.refresh()`, a server action, a query-string change — keeps the boundary and its revealed\n * content, which is what makes those updates seamless. A section whose state is meant to outlive a route\n * change wants a bare {@link CatchBoundary} (with an ancestor's `Suspense`, or none) rather than an\n * `AsyncBoundary`.\n *\n * @example\n * ```tsx\n * import { AsyncBoundary } from '@rshono/core/client';\n *\n * <AsyncBoundary loading={<Spinner />} error={(e, reset) => <Retry onClick={reset} />}>\n * <SlowServerComponent />\n * </AsyncBoundary>\n * ```\n *\n * @see {@link https://react.dev/reference/react/Suspense | React — `<Suspense>`}\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n */\nexport function AsyncBoundary({ loading, error, onError, resetKeys, children }: AsyncBoundaryProps): ReactNode {\n // The pathname as the key is what makes a navigation mount this boundary rather than reconcile into\n // the one the outgoing route revealed — see the component's docs. `undefined` outside a page's tree\n // keys nothing, which keeps a boundary rendered somewhere else behaving as it always has.\n const pathname = useNavigationPathname();\n return (\n <CatchBoundary key={pathname} fallback={error} onError={onError} resetKeys={resetKeys}>\n <Suspense fallback={loading}>{children}</Suspense>\n </CatchBoundary>\n );\n}\n"]}
@@ -69,6 +69,14 @@ export declare function RouterProvider({ href, params, children }: {
69
69
  params: Record<string, string>;
70
70
  children: ReactNode;
71
71
  }): import("react").JSX.Element;
72
+ /**
73
+ * The pathname of the payload on screen, for framework components that key themselves to the route.
74
+ * Unlike {@link useNavigation} it tolerates being rendered outside a page: with no navigation context to
75
+ * read it answers `undefined`, so the caller can treat the absent route as "no key".
76
+ *
77
+ * @internal
78
+ */
79
+ export declare function useNavigationPathname(): string | undefined;
72
80
  /**
73
81
  * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
74
82
  * `'use client'` component.
@@ -49,6 +49,16 @@ export function RouterProvider({ href, params, children }) {
49
49
  }, [href, hash, params, router]);
50
50
  return _jsx(NavigationContext.Provider, { value: value, children: children });
51
51
  }
52
+ /**
53
+ * The pathname of the payload on screen, for framework components that key themselves to the route.
54
+ * Unlike {@link useNavigation} it tolerates being rendered outside a page: with no navigation context to
55
+ * read it answers `undefined`, so the caller can treat the absent route as "no key".
56
+ *
57
+ * @internal
58
+ */
59
+ export function useNavigationPathname() {
60
+ return useContext(NavigationContext)?.url.pathname;
61
+ }
52
62
  /**
53
63
  * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a
54
64
  * `'use client'` component.
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,EAAkB,MAAM,OAAO,CAAC;AAyDjG,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,aAAyB;IAChD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IACrD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AACvE,CAAC;AAED,iDAAiD;AACjD,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,GAAW,EAAE,CAAC,EAAE,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,2GAA2G;IAC3G,yGAAyG;IACzG,MAAM,IAAI,GAAG,oBAAoB,CAAC,eAAe,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE;QAC1C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjC,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, useSyncExternalStore, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * — it is not written back to the address bar.\n *\n * The path, query and origin travel in the page payload, but the fragment cannot: a browser leaves `#…`\n * out of the request line, so the server renders every page without one. `url.hash` is therefore read\n * from the browser after hydration and follows `hashchange` — an in-page link, Back/Forward between\n * anchors of one document, or opening the document at one. Nothing else about the URL is affected; see\n * {@link useNavigation} for the `render: 'static'` case.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * The browser's fragment navigation: the one part of the address a payload can never carry, and the one part\n * that can move without a page data request. `hashchange` is every way it moves within a document — an\n * in-page link, Back/Forward between anchors of one page, opening the document at `#section` covered by the\n * post-hydration check `useSyncExternalStore` makes for a changed snapshot. A cross-page `#anchor` commits a\n * payload instead; the re-render that follows reads the fragment then.\n */\nfunction subscribeToHash(onStoreChange: () => void): () => void {\n window.addEventListener('hashchange', onStoreChange);\n return () => window.removeEventListener('hashchange', onStoreChange);\n}\n\n/** The live fragment, `#…` included, or `''`. */\nconst readHash = (): string => window.location.hash;\n\n/**\n * What the fragment reads as while the server snapshot is in use — server render and hydration. The server\n * never saw one, so the payload's URL has none; answering with the live fragment here would render markup\n * the server did not and fail hydration. The empty string is exactly what the payload carries, and the\n * post-hydration re-render that `useSyncExternalStore` performs for a changed snapshot is where the\n * browser's own arrives.\n */\nconst readServerHash = (): string => '';\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n // `href` is the payload's URL — see `subscribeToHash` — so the browser's fragment is applied on top of it.\n // This is the only client-side part of `url`; path, query and origin stay exactly what the payload said.\n const hash = useSyncExternalStore(subscribeToHash, readHash, readServerHash);\n const value = useMemo<NavigationState>(() => {\n const url = new URL(href);\n url.hash = hash;\n return { url, params, router };\n }, [href, hash, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * The fragment is the exception the payload cannot cover: a browser never sends `#…` to the server, so\n * `url.hash` is read from the address bar after hydration and kept in sync on `hashchange` — an in-page\n * link, Back/Forward between anchors, or opening the document at `#section` — with no request either way.\n * Everything before the `#` remains what the payload carried.\n *\n * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The\n * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own\n * `PageProps.url` — the same value, not a live one, `url.hash` aside. A page whose output depends on the\n * query wants `render: 'dynamic'`; a component that only needs it after hydration can read\n * `location.search` in an effect.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/runtime/navigation.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,EAAkB,MAAM,OAAO,CAAC;AAyDjG,MAAM,IAAI,GAAG,GAAG,EAAE,GAAE,CAAC,CAAC;AAEtB,MAAM,aAAa,GAAqB,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAEhI;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAmB,aAAa,CAAC,CAAC;AAE5E,MAAM,iBAAiB,GAAG,aAAa,CAAyB,IAAI,CAAC,CAAC;AAEtE;;;;;;GAMG;AACH,SAAS,eAAe,CAAC,aAAyB;IAChD,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;IACrD,OAAO,GAAG,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,YAAY,EAAE,aAAa,CAAC,CAAC;AACvE,CAAC;AAED,iDAAiD;AACjD,MAAM,QAAQ,GAAG,GAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,GAAW,EAAE,CAAC,EAAE,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAyE;IAC9H,MAAM,MAAM,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IACzC,2GAA2G;IAC3G,yGAAyG;IACzG,MAAM,IAAI,GAAG,oBAAoB,CAAC,eAAe,EAAE,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,OAAO,CAAkB,GAAG,EAAE;QAC1C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1B,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAChB,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjC,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAEjC,OAAO,KAAC,iBAAiB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAA8B,CAAC;AAC3F,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,UAAU,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC,QAAQ,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,KAAK,GAAG,UAAU,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,mKAAmK,CACpK,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC","sourcesContent":["'use client';\n\nimport { createContext, useContext, useMemo, useSyncExternalStore, type ReactNode } from 'react';\n\n/**\n * Imperative navigation actions, reached as `useNavigation().router`.\n *\n * Every action is a **soft** navigation: the page's flight payload is fetched and applied in place, so\n * client component state outside the changed subtree survives. Off-site hrefs — and a traversal that leaves\n * the app — fall back to a full load.\n *\n * Soft navigation is the browser's\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigation_API | Navigation API}; where that is\n * missing, every action below is still correct and simply performs a real browser load.\n *\n * @example\n * ```tsx\n * const { router } = useNavigation();\n * router.push('/dashboard'); // navigate, new history entry\n * router.replace('/login'); // navigate, no new entry\n * router.back(); // one entry back, as the browser's button does\n * router.forward(); // one entry forward\n * router.refresh(); // re-run this route's server components\n * ```\n */\nexport interface NavigationRouter {\n /** Navigates to `href` and pushes a new history entry. */\n push(href: string): void;\n /** Navigates to `href`, replacing the current history entry instead of adding one. */\n replace(href: string): void;\n /** Steps one entry back in the browser's session history. Nothing to go back to is a no-op. */\n back(): void;\n /** Steps one entry forward in the browser's session history. A no-op on the newest entry. */\n forward(): void;\n /** Re-fetches the current route from the server, re-running its server components. */\n refresh(): void;\n /** `true` while a soft navigation is in flight — use it to disable controls or show a spinner. */\n pending: boolean;\n}\n\n/** The current location plus the {@link NavigationRouter}, as returned by {@link useNavigation}. */\nexport interface NavigationState {\n /**\n * The full current {@link URL}. A fresh instance per navigation, so mutating it affects nothing else\n * — it is not written back to the address bar.\n *\n * The path, query and origin travel in the page payload, but the fragment cannot: a browser leaves `#…`\n * out of the request line, so the server renders every page without one. `url.hash` is therefore read\n * from the browser after hydration and follows `hashchange` — an in-page link, Back/Forward between\n * anchors of one document, or opening the document at one. Nothing else about the URL is affected; see\n * {@link useNavigation} for the `render: 'static'` case.\n */\n url: URL;\n /** Matched route params for the current page, e.g. `{ id: '42' }` for `/profile/:id`. */\n params: Record<string, string>;\n /** Imperative navigation actions and the `pending` flag. */\n router: NavigationRouter;\n}\n\nconst noop = () => {};\n\nconst defaultRouter: NavigationRouter = { push: noop, replace: noop, back: noop, forward: noop, refresh: noop, pending: false };\n\n/**\n * Carries the live {@link NavigationRouter} from the hydration runtime down to {@link RouterProvider}.\n *\n * @internal\n */\nexport const RouterContext = createContext<NavigationRouter>(defaultRouter);\n\nconst NavigationContext = createContext<NavigationState | null>(null);\n\n/**\n * The browser's fragment navigation: the one part of the address a payload can never carry, and the one part\n * that can move without a page data request. `hashchange` is every way it moves within a document — an\n * in-page link, Back/Forward between anchors of one page, opening the document at `#section` covered by the\n * post-hydration check `useSyncExternalStore` makes for a changed snapshot. A cross-page `#anchor` commits a\n * payload instead; the re-render that follows reads the fragment then.\n */\nfunction subscribeToHash(onStoreChange: () => void): () => void {\n window.addEventListener('hashchange', onStoreChange);\n return () => window.removeEventListener('hashchange', onStoreChange);\n}\n\n/** The live fragment, `#…` included, or `''`. */\nconst readHash = (): string => window.location.hash;\n\n/**\n * What the fragment reads as while the server snapshot is in use — server render and hydration. The server\n * never saw one, so the payload's URL has none; answering with the live fragment here would render markup\n * the server did not and fail hydration. The empty string is exactly what the payload carries, and the\n * post-hydration re-render that `useSyncExternalStore` performs for a changed snapshot is where the\n * browser's own arrives.\n */\nconst readServerHash = (): string => '';\n\n/**\n * Publishes the per-render location and params for {@link useNavigation} to read. The RSC entry wraps\n * every page in one.\n *\n * @internal\n */\nexport function RouterProvider({ href, params, children }: { href: string; params: Record<string, string>; children: ReactNode }) {\n const router = useContext(RouterContext);\n // `href` is the payload's URL — see `subscribeToHash` — so the browser's fragment is applied on top of it.\n // This is the only client-side part of `url`; path, query and origin stay exactly what the payload said.\n const hash = useSyncExternalStore(subscribeToHash, readHash, readServerHash);\n const value = useMemo<NavigationState>(() => {\n const url = new URL(href);\n url.hash = hash;\n return { url, params, router };\n }, [href, hash, params, router]);\n\n return <NavigationContext.Provider value={value}>{children}</NavigationContext.Provider>;\n}\n\n/**\n * The pathname of the payload on screen, for framework components that key themselves to the route.\n * Unlike {@link useNavigation} it tolerates being rendered outside a page: with no navigation context to\n * read it answers `undefined`, so the caller can treat the absent route as \"no key\".\n *\n * @internal\n */\nexport function useNavigationPathname(): string | undefined {\n return useContext(NavigationContext)?.url.pathname;\n}\n\n/**\n * Reactive access to the current URL and programmatic navigation, in one hook. Call it from a\n * `'use client'` component.\n *\n * `url` and `params` are computed on the server and travel in the flight payload, so they are correct\n * during SSR — no hydration flicker — and update on every navigation. `router` holds the imperative\n * actions plus a `pending` flag, `true` while a soft navigation is in flight.\n *\n * The fragment is the exception the payload cannot cover: a browser never sends `#…` to the server, so\n * `url.hash` is read from the address bar after hydration and kept in sync on `hashchange` — an in-page\n * link, Back/Forward between anchors, or opening the document at `#section` — with no request either way.\n * Everything before the `#` remains what the payload carried.\n *\n * **On a `render: 'static'` route `url` is frozen at build time**, origin included and query empty. The\n * payload is one prerendered set of bytes and this reads the `href` in it, so it is the page's own\n * `PageProps.url` — the same value, not a live one, `url.hash` aside. A page whose output depends on the\n * query wants `render: 'dynamic'`; a component that only needs it after hydration can read\n * `location.search` in an effect.\n *\n * Hooks can't run in a server component; read the same data there from `getRequestContext()`.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useNavigation } from '@rshono/core/client';\n *\n * export function NextPage() {\n * const { url, router } = useNavigation();\n * const page = Number(url.searchParams.get('page') ?? '1');\n * return (\n * <button disabled={router.pending} onClick={() => router.push(`${url.pathname}?page=${page + 1}`)}>\n * Next {router.pending ? '…' : ''}\n * </button>\n * );\n * }\n * ```\n *\n * @returns The current {@link NavigationState}: `url` and `params`, plus `router`\n * ({@link NavigationRouter}) with `push` / `replace` / `back` / `forward` / `refresh` / `pending`.\n * @throws If called outside a page's React tree, where there is no navigation\n * context to read.\n *\n * @see {@link https://www.rshono.com/docs/api#rshonocoreclient | Docs — `@rshono/core/client`}\n * @see {@link https://www.rshono.com/docs/pages#client-components | Docs — client components}\n */\nexport function useNavigation(): NavigationState {\n const value = useContext(NavigationContext);\n if (!value) {\n throw new Error(\n \"[rshono] useNavigation() must be called inside a 'use client' component rendered by a page. In a server component, read the URL from getRequestContext() instead.\",\n );\n }\n return value;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/core",
3
- "version": "1.0.0-rc.26",
3
+ "version": "1.0.0-rc.27",
4
4
  "description": "Minimalist web framework — Hono + Rspack + React Server Components",
5
5
  "author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
6
6
  "license": "MIT",