@nifrajs/web-react 3.5.0 → 4.0.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/dist/router.js CHANGED
@@ -13,6 +13,8 @@
13
13
  // `/client`, not the root - these are DOM values, and the root's graph carries the
14
14
  // server, which Vite's dev server evaluates rather than tree-shakes.
15
15
  import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } from "@nifrajs/web/client";
16
+ // Its own subpath, so an app that never calls `useMatches` bundles none of it.
17
+ import { chainMatches } from "@nifrajs/web/internal/matches-runtime";
16
18
  import { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
17
19
  // Frozen empty params/search so the default context value has a stable reference (no needless re-renders).
18
20
  const EMPTY_PARAMS = Object.freeze({});
@@ -73,6 +75,24 @@ export function useParams() {
73
75
  export function useSearch() {
74
76
  return useContext(RouterContext).search;
75
77
  }
78
+ const NO_MATCHES = Object.freeze([]);
79
+ /**
80
+ * The rendered chain, outermost layout first and the page last: each module's `id`, the URL
81
+ * `pathname` it wraps, its `params` and loader `data`, and its `handle` export. The same list on the
82
+ * server and in the browser, so a layout can render breadcrumbs or read a flag the page exports:
83
+ *
84
+ * ```tsx
85
+ * export const handle = { crumb: "Settings" } // in a page or a layout
86
+ * // in a layout above it:
87
+ * const crumbs = useMatches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
88
+ * ```
89
+ *
90
+ * Empty outside a nifra route tree.
91
+ */
92
+ export function useMatches() {
93
+ const props = useContext(RouterContext).renderProps;
94
+ return useMemo(() => (props === undefined ? NO_MATCHES : chainMatches(props)), [props]);
95
+ }
76
96
  /** The current {@link Location} (`pathname`/`search`/`hash`), derived from the router context. */
77
97
  export function useLocation() {
78
98
  const { path } = useContext(RouterContext);
@@ -204,7 +224,7 @@ function isPlainLeftClick(event, target) {
204
224
  * instead of a full reload. Calling `navigate` + `preventDefault` here means `installHistory`'s
205
225
  * document-level click handler sees `defaultPrevented` and stands down - exactly one navigation.
206
226
  */
207
- export const Link = forwardRef(function Link({ to, replace, onClick, target, ...rest }, ref) {
227
+ export const Link = forwardRef(function Link({ to, replace, prefetch, onClick, target, ...rest }, ref) {
208
228
  const handleClick = (event) => {
209
229
  onClick?.(event);
210
230
  if (event.defaultPrevented)
@@ -217,7 +237,14 @@ export const Link = forwardRef(function Link({ to, replace, onClick, target, ...
217
237
  event.preventDefault();
218
238
  navigate(to, { replace: replace === true });
219
239
  };
220
- return createElement("a", { ...rest, href: to, target, onClick: handleClick, ref });
240
+ return createElement("a", {
241
+ ...rest,
242
+ href: to,
243
+ target,
244
+ onClick: handleClick,
245
+ ref,
246
+ ...(prefetch !== undefined ? { "data-nifra-prefetch": prefetch } : {}),
247
+ });
221
248
  });
222
249
  /** Normalize a pathname for comparison: optional case-fold + drop a single trailing slash (but keep a
223
250
  * bare `"/"`). */
@@ -1 +1 @@
1
- {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/web-react",
3
- "version": "3.5.0",
3
+ "version": "4.0.0",
4
4
  "description": "React render adapter for @nifrajs/web - SSR + hydration (Bun-native JSX, no Babel).",
5
5
  "keywords": [
6
6
  "bun",
@@ -14,6 +14,9 @@
14
14
  "full-stack"
15
15
  ],
16
16
  "type": "module",
17
+ "nifra": {
18
+ "environment": "library"
19
+ },
17
20
  "main": "./dist/index.js",
18
21
  "module": "./dist/index.js",
19
22
  "types": "./dist/index.d.ts",
@@ -58,6 +61,11 @@
58
61
  "types": "./dist/i18n.d.ts",
59
62
  "default": "./dist/i18n.js"
60
63
  },
64
+ "./auth": {
65
+ "bun": "./src/auth.ts",
66
+ "types": "./dist/auth.d.ts",
67
+ "default": "./dist/auth.js"
68
+ },
61
69
  "./image": {
62
70
  "bun": "./src/image.ts",
63
71
  "types": "./dist/image.d.ts",
@@ -87,10 +95,11 @@
87
95
  "access": "public"
88
96
  },
89
97
  "dependencies": {
90
- "@nifrajs/core": "3.5.0",
91
- "@nifrajs/i18n": "3.5.0",
92
- "@nifrajs/image": "3.5.0",
93
- "@nifrajs/web": "3.5.0"
98
+ "@nifrajs/core": "4.0.0",
99
+ "@nifrajs/authjs": "4.0.0",
100
+ "@nifrajs/i18n": "4.0.0",
101
+ "@nifrajs/image": "4.0.0",
102
+ "@nifrajs/web": "4.0.0"
94
103
  },
95
104
  "peerDependencies": {
96
105
  "react": "^19.0.0",
package/src/auth.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `@nifrajs/web-react/auth` - React bindings for `@nifrajs/authjs`. `<AuthSessionProvider>`
3
+ * seeds the subtree with a session (from a loader on the server, refreshed in the browser) and
4
+ * `useAuthSession()` reads it; `signIn`/`signOut` delegate to the shared client. Imports only
5
+ * `react` + `@nifrajs/authjs/client`; no JSX (the package builds with plain `tsc`).
6
+ */
7
+ import {
8
+ type AuthClient,
9
+ createAuthClient,
10
+ type Session,
11
+ type SignInOptions,
12
+ type SignOutOptions,
13
+ } from "@nifrajs/authjs/client"
14
+ import {
15
+ createContext,
16
+ createElement,
17
+ type ReactNode,
18
+ useContext,
19
+ useEffect,
20
+ useMemo,
21
+ useState,
22
+ } from "react"
23
+
24
+ export type { Session }
25
+ export type AuthStatus = "loading" | "authenticated" | "unauthenticated"
26
+
27
+ export interface AuthSession {
28
+ readonly status: AuthStatus
29
+ readonly session: Session | null
30
+ /** Re-read the session from the server (after an out-of-band change, for example). */
31
+ readonly refresh: () => Promise<void>
32
+ readonly signIn: (providerId: string, options?: SignInOptions) => Promise<void>
33
+ readonly signOut: (options?: SignOutOptions) => Promise<void>
34
+ }
35
+
36
+ const AuthSessionContext = createContext<AuthSession | null>(null)
37
+
38
+ export interface AuthSessionProviderProps {
39
+ /** Shared client (base path, custom fetch). Default `createAuthClient()`. */
40
+ readonly client?: AuthClient
41
+ /** SSR seed from a loader (`await getSession(request, config)`). `undefined` (default) fetches
42
+ * on mount instead - the subtree renders `loading` until it resolves. */
43
+ readonly initialSession?: Session | null
44
+ readonly children?: ReactNode
45
+ }
46
+
47
+ function seededStatus(seed: Session | null | undefined): AuthStatus {
48
+ return seed === undefined ? "loading" : seed === null ? "unauthenticated" : "authenticated"
49
+ }
50
+
51
+ /** Provide the Auth.js session to the subtree. Memoized on client + seed; refresh re-reads. A new
52
+ * `initialSession` (a loader re-run on navigation) replaces the session, as a remount would. */
53
+ export function AuthSessionProvider(props: AuthSessionProviderProps): ReactNode {
54
+ const client = useMemo(() => props.client ?? createAuthClient(), [props.client])
55
+ const [seed, setSeed] = useState(props.initialSession)
56
+ const [status, setStatus] = useState<AuthStatus>(seededStatus(props.initialSession))
57
+ const [session, setSession] = useState<Session | null>(props.initialSession ?? null)
58
+ if (seed !== props.initialSession) {
59
+ // A render-phase update: React re-runs this component before any child sees the old session.
60
+ setSeed(props.initialSession)
61
+ setStatus(seededStatus(props.initialSession))
62
+ setSession(props.initialSession ?? null)
63
+ }
64
+ const refresh = useMemo(() => {
65
+ return async (): Promise<void> => {
66
+ try {
67
+ const next = await client.getSession()
68
+ setSession(next)
69
+ setStatus(next === null ? "unauthenticated" : "authenticated")
70
+ } catch {
71
+ // A failed session read must fail closed as anonymous; leaving "loading" can strand the
72
+ // subtree in an auth-pending state and make protected UI appear available by omission.
73
+ setSession(null)
74
+ setStatus("unauthenticated")
75
+ }
76
+ }
77
+ }, [client])
78
+ useEffect(() => {
79
+ let live = true
80
+ if (props.initialSession === undefined) {
81
+ void client.getSession().then(
82
+ (next) => {
83
+ if (!live) return
84
+ setSession(next)
85
+ setStatus(next === null ? "unauthenticated" : "authenticated")
86
+ },
87
+ () => {
88
+ // Treat an unavailable session endpoint as anonymous rather than keeping auth state
89
+ // pending indefinitely or exposing stale authenticated UI.
90
+ if (!live) return
91
+ setSession(null)
92
+ setStatus("unauthenticated")
93
+ },
94
+ )
95
+ }
96
+ return () => {
97
+ live = false
98
+ }
99
+ }, [client, props.initialSession])
100
+ const value = useMemo<AuthSession>(
101
+ () => ({
102
+ status,
103
+ session,
104
+ refresh,
105
+ signIn: (providerId, options) => client.signIn(providerId, options),
106
+ signOut: async (options) => {
107
+ await client.signOut(options)
108
+ // The server ended the session: the subtree must not go on showing it, whether a redirect is
109
+ // pending or `redirect: false` keeps the page.
110
+ setSession(null)
111
+ setStatus("unauthenticated")
112
+ },
113
+ }),
114
+ [status, session, refresh, client],
115
+ )
116
+ return createElement(AuthSessionContext.Provider, { value }, props.children)
117
+ }
118
+
119
+ /** Read the Auth.js session (`{ status, session }`). Throws if no `<AuthSessionProvider>` is above. */
120
+ export function useAuthSession(): AuthSession {
121
+ const session = useContext(AuthSessionContext)
122
+ if (session === null) {
123
+ throw new Error(
124
+ "[nifra/web-react] useAuthSession() must be used within an <AuthSessionProvider>",
125
+ )
126
+ }
127
+ return session
128
+ }
package/src/client.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  import type { MountRouterOptions, RenderProps } from "@nifrajs/web"
2
- // `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
3
- // instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
4
- import { searchOfChain } from "@nifrajs/web/client"
5
2
  /**
6
3
  * @nifrajs/web-react/client - React client runtime. `hydrate` hydrates a single SSR'd route;
7
4
  * `mountRouter` hydrates a stateful Router that subscribes to the agnostic store (via
@@ -13,6 +10,7 @@ import { createElement, type FunctionComponent, useSyncExternalStore } from "rea
13
10
  import { hydrateRoot } from "react-dom/client"
14
11
  import { compose } from "./compose.ts"
15
12
  import { setMountedRouter } from "./fetcher.ts"
13
+ import { routeProps } from "./route-props.ts"
16
14
 
17
15
  // The `_error` boundary chain element - defined in its own (react-dom-free) module, re-exported here so
18
16
  // nifra's client codegen resolves it from `@nifrajs/web-react/client` alongside `mountRouter`.
@@ -58,29 +56,16 @@ export const hydrationAssuranceHook = Object.freeze({
58
56
  * SSR markup on hydration.
59
57
  */
60
58
  export function mountRouter(options: MountRouterOptions): void {
61
- const { router, routes, searchSchemas, container } = options
59
+ const { router, routes, searchSchemas, matchChains, container } = options
62
60
  setMountedRouter(router) // expose it to useFetcher/useFetchers (same page, client-only)
61
+ // What the server rendered. React hydrates against THIS, whatever the store says by the time it gets
62
+ // to the component - hydration is scheduled, and a store that moved on first (a client loader that
63
+ // answered, a held `ssr = false` page released) would otherwise hydrate a tree the markup never had.
64
+ // React then re-renders with the live snapshot on its own.
65
+ const hydrated = router.snapshot()
63
66
  const Router: FunctionComponent = () => {
64
- const state = useSyncExternalStore(router.subscribe, router.snapshot, router.snapshot)
65
- // Derive this route's typed `search` from the URL query + the route's schema CHAIN (layout schemas +
66
- // page, merged page-wins), the SAME `searchOfChain` the server ran, so `useSearch` reads an identical
67
- // value and hydrates with no drift.
68
- const q = state.path.indexOf("?")
69
- const rawSearch = q === -1 ? "" : state.path.slice(q)
70
- return compose(routes[state.routeId] ?? [], {
71
- data: state.data,
72
- actionData: state.actionData,
73
- pending: state.pending,
74
- ...(state.pendingPath !== undefined ? { pendingPath: state.pendingPath } : {}),
75
- // `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext -
76
- // sourced from router state here, matching the SSR render's request-derived values on hydration.
77
- params: state.params,
78
- path: state.path,
79
- search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
80
- // The in-flight submission (for optimistic UI) - spread only when present.
81
- ...(state.submission ? { submission: state.submission } : {}),
82
- ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
83
- })
67
+ const state = useSyncExternalStore(router.subscribe, router.snapshot, () => hydrated)
68
+ return compose(routes[state.routeId] ?? [], routeProps(state, searchSchemas, matchChains))
84
69
  }
85
70
  const onRecoverableError = assuranceError()
86
71
  hydrateRoot(
package/src/compose.ts CHANGED
@@ -46,6 +46,7 @@ export function compose(chain: readonly unknown[], props: RenderProps): ReactNod
46
46
  search: props.search ?? EMPTY_SEARCH,
47
47
  pending: props.pending ?? false,
48
48
  pendingPath: props.pendingPath,
49
+ renderProps: props,
49
50
  },
50
51
  },
51
52
  node,
package/src/i18n.ts CHANGED
@@ -5,28 +5,40 @@
5
5
  * and the client rebuilds the same formatter from the same props (no mismatch). Imports only `react` +
6
6
  * `@nifrajs/i18n`; no JSX (the package builds with plain `tsc`).
7
7
  */
8
- import { createFormatter, type Formatter, type Messages } from "@nifrajs/i18n"
9
- import { createContext, createElement, type ReactNode, useContext, useMemo } from "react"
8
+ import {
9
+ createFormatter,
10
+ type Formatter,
11
+ type FormatterOptions,
12
+ type MessageKey,
13
+ type RegisteredMessages,
14
+ type Translation,
15
+ } from "@nifrajs/i18n"
16
+ import { type RichChunks, type RichRenderer, renderRich } from "@nifrajs/i18n/rich"
17
+ import { createContext, createElement, Fragment, type ReactNode, useContext, useMemo } from "react"
10
18
 
11
19
  const I18nContext = createContext<Formatter | null>(null)
12
20
 
13
- export interface I18nProviderProps {
21
+ export interface I18nProviderProps extends FormatterOptions {
14
22
  readonly locale: string
15
- readonly messages: Messages
23
+ /** The catalog for `locale`, checked against the registered catalog type when one is declared. */
24
+ readonly messages: Translation
16
25
  readonly children?: ReactNode
17
26
  }
18
27
 
19
- /** Provide a {@link Formatter} (built from `locale` + `messages`) to the subtree. Memoized on
20
- * `locale`/`messages`, so switching locale rebuilds it and re-renders consumers. */
28
+ /** Provide a {@link Formatter} (built from `locale` + `messages`, with the optional `fallback`,
29
+ * `onMissing`, `timeZone` and `numberingSystem` of `createFormatter`) to the subtree. Memoized
30
+ * on those props, so switching locale rebuilds it and re-renders consumers; formatters are cached by
31
+ * catalog identity, so an inline `fallback={[en]}` still yields the same instance. */
21
32
  export function I18nProvider(props: I18nProviderProps): ReactNode {
33
+ const { locale, messages, fallback, onMissing, timeZone, numberingSystem } = props
22
34
  const formatter = useMemo(
23
- () => createFormatter(props.locale, props.messages),
24
- [props.locale, props.messages],
35
+ () => createFormatter(locale, messages, { fallback, onMissing, timeZone, numberingSystem }),
36
+ [locale, messages, fallback, onMissing, timeZone, numberingSystem],
25
37
  )
26
38
  return createElement(I18nContext.Provider, { value: formatter }, props.children)
27
39
  }
28
40
 
29
- /** Read the current {@link Formatter} (`{ locale, t, n, d }`). Throws if no `<I18nProvider>` is above. */
41
+ /** Read the current {@link Formatter} (`{ locale, t, get, n, d }`). Throws if no `<I18nProvider>` is above. */
30
42
  export function useT(): Formatter {
31
43
  const formatter = useContext(I18nContext)
32
44
  if (formatter === null) {
@@ -34,3 +46,31 @@ export function useT(): Formatter {
34
46
  }
35
47
  return formatter
36
48
  }
49
+
50
+ /** Tag handlers for {@link rich}, by tag name: each receives its tag's content as one node. */
51
+ export type RichTags = Readonly<Record<string, (content: ReactNode) => ReactNode>>
52
+
53
+ // Children passed as arguments, not an array, so React asks for no keys.
54
+ const join = (chunks: RichChunks<ReactNode>): ReactNode =>
55
+ chunks.length === 0
56
+ ? null
57
+ : chunks.length === 1
58
+ ? chunks[0]
59
+ : createElement(Fragment, null, ...chunks)
60
+ const REACT_RICH: RichRenderer<ReactNode> = { join, lineBreak: () => createElement("br") }
61
+
62
+ /**
63
+ * The message at `key` with its tags rendered by `tags`, as React nodes - no HTML, no
64
+ * `dangerouslySetInnerHTML`. `"Read the <link>terms</link>"` with
65
+ * `{ link: (content) => <a href="/terms">{content}</a> }` renders the link around "terms"; a tag with
66
+ * no handler renders its content as text, `<br/>` is a `<br>`, and interpolated values are always
67
+ * text. See `@nifrajs/i18n/rich`.
68
+ */
69
+ export function rich<M extends object = RegisteredMessages>(
70
+ formatter: Formatter<M>,
71
+ key: MessageKey<M>,
72
+ tags?: RichTags,
73
+ vars?: Readonly<Record<string, unknown>>,
74
+ ): ReactNode {
75
+ return renderRich(REACT_RICH, formatter, key, tags, vars)
76
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import type { RenderAdapter } from "@nifrajs/web"
6
6
  */
7
7
  import { compose } from "./compose.ts"
8
8
  import { reactDomServer } from "./react-dom-server.ts"
9
+ import { explainRenderError } from "./render-error.ts"
9
10
 
10
11
  /** The React server render adapter - pass to @nifrajs/web's `renderPage`. */
11
12
  export const reactAdapter: RenderAdapter = {
@@ -19,14 +20,25 @@ export const reactAdapter: RenderAdapter = {
19
20
  // renderPage awaits it.
20
21
  async renderToString(chain, props) {
21
22
  const { renderToString } = await reactDomServer()
22
- return renderToString(compose(chain, props))
23
+ try {
24
+ return renderToString(compose(chain, props))
25
+ } catch (error) {
26
+ throw explainRenderError(error)
27
+ }
23
28
  },
24
- async renderToStream(chain, props) {
29
+ async renderToStream(chain, props, options) {
25
30
  // Resolves a Web `ReadableStream<Uint8Array>` once the shell is renderable; Suspense
26
31
  // boundaries stream as they resolve. No `bootstrapModules` - nifra injects the client entry in
27
32
  // the document tail. React's default `onError` logs to console.error (errors aren't swallowed).
33
+ // The nonce reaches the inline runtime React streams to reveal each resolved boundary.
28
34
  const { renderToReadableStream } = await reactDomServer()
29
- return renderToReadableStream(compose(chain, props))
35
+ try {
36
+ return await (options?.nonce === undefined
37
+ ? renderToReadableStream(compose(chain, props))
38
+ : renderToReadableStream(compose(chain, props), { nonce: options.nonce }))
39
+ } catch (error) {
40
+ throw explainRenderError(error)
41
+ }
30
42
  },
31
43
  // React reconciles against the existing DOM on hydrate, so no per-document bootstrap
32
44
  // script is needed (contrast Solid's generateHydrationScript) - the seam allows both.
@@ -29,7 +29,10 @@ import type { ReactNode } from "react"
29
29
  * resolves to an absolute path string) stays strict - no `any` crosses the boundary. */
30
30
  export interface ReactDomServer {
31
31
  renderToString(node: ReactNode): string
32
- renderToReadableStream(node: ReactNode): Promise<ReadableStream<Uint8Array>>
32
+ renderToReadableStream(
33
+ node: ReactNode,
34
+ options?: { readonly nonce?: string },
35
+ ): Promise<ReadableStream<Uint8Array>>
33
36
  }
34
37
 
35
38
  interface RuntimeProcess {
@@ -0,0 +1,18 @@
1
+ /** A hook read a null dispatcher: the component's React is not the one the renderer set up. JSC and V8
2
+ * word the null dereference differently; the property read is always a hook. */
3
+ const NO_DISPATCHER =
4
+ /ReactSharedInternals\.H\.|resolveDispatcher\(\)\.|Cannot read properties of null \(reading 'use[A-Za-z]*'\)/
5
+
6
+ /** Name the cause of a null-dispatcher crash. The app-root check in ./react-dom-server catches a
7
+ * renderer on the wrong copy; this covers a component that brings its own (a linked package's). */
8
+ export function explainRenderError(error: unknown): unknown {
9
+ if (!(error instanceof TypeError) || !NO_DISPATCHER.test(error.message)) return error
10
+ return new Error(
11
+ "[nifra/web-react] a component called a React hook with no dispatcher, so two copies of React " +
12
+ "reached SSR: the one react-dom renders with, and another the component imports (often a linked " +
13
+ "package's own node_modules). Dedupe react to one physical copy: `nifra check` names the " +
14
+ "importer of each, and the package.json `nifra.singleCopy` declaration redirects them. " +
15
+ `See https://nifra.dev/docs/troubleshooting. (${error.message})`,
16
+ { cause: error },
17
+ )
18
+ }
@@ -0,0 +1,37 @@
1
+ import type { MountRouterOptions, RenderProps, RouterState } from "@nifrajs/web"
2
+ // `/client`, not the root: the root's graph carries the server, and Vite's dev server evaluates it
3
+ // instead of tree-shaking it - which broke hydration before the browser ran a line of app code.
4
+ import { searchOfChain } from "@nifrajs/web/client"
5
+
6
+ /** The client half of the server's `RenderProps`, so the first render matches the SSR markup. */
7
+ export function routeProps(
8
+ state: RouterState,
9
+ searchSchemas: MountRouterOptions["searchSchemas"],
10
+ matchChains?: MountRouterOptions["matchChains"],
11
+ ): RenderProps {
12
+ const matchChain = matchChains?.[state.routeId]
13
+ // Derive this route's typed `search` from the URL query + the route's schema CHAIN (layout schemas +
14
+ // page, merged page-wins), the SAME `searchOfChain` the server ran, so `useSearch` reads an identical
15
+ // value and hydrates with no drift.
16
+ const q = state.path.indexOf("?")
17
+ const rawSearch = q === -1 ? "" : state.path.slice(q)
18
+ return {
19
+ data: state.data,
20
+ // Each layout's own loader data. Without it a layout renders `data: null` on the client while the
21
+ // server rendered it with its data - a hydration mismatch on the first paint.
22
+ ...(state.layoutData !== undefined ? { layoutData: state.layoutData } : {}),
23
+ actionData: state.actionData,
24
+ pending: state.pending,
25
+ ...(state.pendingPath !== undefined ? { pendingPath: state.pendingPath } : {}),
26
+ // `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext -
27
+ // sourced from router state here, matching the SSR render's request-derived values on hydration.
28
+ params: state.params,
29
+ path: state.path,
30
+ search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
31
+ // The in-flight submission (for optimistic UI) - spread only when present.
32
+ ...(state.submission ? { submission: state.submission } : {}),
33
+ ...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
34
+ // The chain `useMatches` reports - the server passed the same one to the SSR render.
35
+ ...(matchChain !== undefined ? { matchChain } : {}),
36
+ }
37
+ }
package/src/router.ts CHANGED
@@ -22,6 +22,9 @@ import type {
22
22
  NavigateFunction,
23
23
  NavigateOptions,
24
24
  NavigateTargetInput,
25
+ PrefetchMode,
26
+ RenderProps,
27
+ UIMatch,
25
28
  } from "@nifrajs/web"
26
29
  // `/client`, not the root - these are DOM values, and the root's graph carries the
27
30
  // server, which Vite's dev server evaluates rather than tree-shakes.
@@ -31,6 +34,8 @@ import {
31
34
  registerBlocker,
32
35
  resolveNavigate,
33
36
  } from "@nifrajs/web/client"
37
+ // Its own subpath, so an app that never calls `useMatches` bundles none of it.
38
+ import { chainMatches } from "@nifrajs/web/internal/matches-runtime"
34
39
  import {
35
40
  type AnchorHTMLAttributes,
36
41
  type CSSProperties,
@@ -47,7 +52,7 @@ import {
47
52
  useState,
48
53
  } from "react"
49
54
 
50
- export type { Blocker, BlockerFunction, BlockerState, NavigateFunction }
55
+ export type { Blocker, BlockerFunction, BlockerState, NavigateFunction, PrefetchMode, UIMatch }
51
56
 
52
57
  /** The current route the routing hooks read. Provided by `compose` on SSR + client mount alike. */
53
58
  export interface RouterContextValue {
@@ -66,6 +71,8 @@ export interface RouterContextValue {
66
71
  /** The `pathname + search` a navigation is transitioning TO while `pending` (`undefined` when idle or
67
72
  * during a same-route revalidation). Powers {@link NavLink}'s per-link `isPending`. */
68
73
  readonly pendingPath?: string | undefined
74
+ /** The render's props, which {@link useMatches} derives the matches from. */
75
+ readonly renderProps?: RenderProps | undefined
69
76
  }
70
77
 
71
78
  // Frozen empty params/search so the default context value has a stable reference (no needless re-renders).
@@ -141,6 +148,26 @@ export function useSearch<
141
148
  : Record<string, unknown>
142
149
  }
143
150
 
151
+ const NO_MATCHES: readonly UIMatch[] = Object.freeze([])
152
+
153
+ /**
154
+ * The rendered chain, outermost layout first and the page last: each module's `id`, the URL
155
+ * `pathname` it wraps, its `params` and loader `data`, and its `handle` export. The same list on the
156
+ * server and in the browser, so a layout can render breadcrumbs or read a flag the page exports:
157
+ *
158
+ * ```tsx
159
+ * export const handle = { crumb: "Settings" } // in a page or a layout
160
+ * // in a layout above it:
161
+ * const crumbs = useMatches().flatMap((m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? [])
162
+ * ```
163
+ *
164
+ * Empty outside a nifra route tree.
165
+ */
166
+ export function useMatches(): readonly UIMatch[] {
167
+ const props = useContext(RouterContext).renderProps
168
+ return useMemo(() => (props === undefined ? NO_MATCHES : chainMatches(props)), [props])
169
+ }
170
+
144
171
  /** The parsed current location. `hash` is always `""` - the fragment is client-only and never reaches
145
172
  * the router state / server, so exposing a live hash would hydration-mismatch; read `window.location.hash`
146
173
  * directly (in an effect) if you truly need it. */
@@ -307,12 +334,16 @@ function isPlainLeftClick(event: MouseEvent, target: string | undefined): boolea
307
334
  return target === undefined || target === "" || target === "_self"
308
335
  }
309
336
 
310
- /** {@link Link} props: every `<a>` attribute except `href` (set from `to`), plus `to` + `replace`. */
337
+ /** {@link Link} props: every `<a>` attribute except `href` (set from `to`), plus `to`, `replace` and
338
+ * `prefetch`. */
311
339
  export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
312
340
  /** Same-origin destination path (e.g. `/users/7?tab=posts`). Rendered as the `<a href>`. */
313
341
  readonly to: string
314
342
  /** Replace the current history entry instead of pushing. */
315
343
  readonly replace?: boolean
344
+ /** When the link warms its route ahead of a click - rendered as `data-nifra-prefetch`. Unset, the
345
+ * nearest ancestor's `data-nifra-prefetch` decides, else `intent` (hover or focus). */
346
+ readonly prefetch?: PrefetchMode
316
347
  }
317
348
 
318
349
  /**
@@ -322,7 +353,7 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
322
353
  * document-level click handler sees `defaultPrevented` and stands down - exactly one navigation.
323
354
  */
324
355
  export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
325
- { to, replace, onClick, target, ...rest },
356
+ { to, replace, prefetch, onClick, target, ...rest },
326
357
  ref,
327
358
  ) {
328
359
  const handleClick = (event: MouseEvent<HTMLAnchorElement>): void => {
@@ -334,7 +365,14 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
334
365
  event.preventDefault()
335
366
  navigate(to, { replace: replace === true })
336
367
  }
337
- return createElement("a", { ...rest, href: to, target, onClick: handleClick, ref })
368
+ return createElement("a", {
369
+ ...rest,
370
+ href: to,
371
+ target,
372
+ onClick: handleClick,
373
+ ref,
374
+ ...(prefetch !== undefined ? { "data-nifra-prefetch": prefetch } : {}),
375
+ })
338
376
  })
339
377
 
340
378
  /** The state a {@link NavLink}'s function-form `className`/`style`/`children` receive. */