@nifrajs/web-react 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/await.d.ts +2 -2
- package/dist/await.js +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +13 -6
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts +4 -3
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +8 -4
- package/dist/compose.js.map +1 -1
- package/dist/content.d.ts +2 -2
- package/dist/content.js +2 -2
- package/dist/error.d.ts +1 -1
- package/dist/error.js +3 -3
- package/dist/fetcher.d.ts +2 -2
- package/dist/fetcher.js +4 -4
- package/dist/fn.d.ts +29 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +31 -0
- package/dist/fn.js.map +1 -0
- package/dist/i18n.d.ts +1 -1
- package/dist/i18n.js +1 -1
- package/dist/image.d.ts +3 -3
- package/dist/image.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/island.d.ts +3 -3
- package/dist/island.js +2 -2
- package/dist/query.d.ts +8 -8
- package/dist/query.js +5 -5
- package/dist/react-dom-server.d.ts +7 -7
- package/dist/react-dom-server.js +7 -7
- package/dist/router.d.ts +66 -18
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +80 -15
- package/dist/router.js.map +1 -1
- package/package.json +11 -6
- package/src/await.ts +2 -2
- package/src/client.ts +13 -7
- package/src/compose.ts +9 -4
- package/src/content.ts +2 -2
- package/src/error.ts +3 -3
- package/src/fetcher.ts +4 -4
- package/src/fn.ts +43 -0
- package/src/i18n.ts +1 -1
- package/src/image.ts +3 -3
- package/src/index.ts +7 -7
- package/src/island.ts +3 -3
- package/src/query.ts +9 -9
- package/src/react-dom-server.ts +9 -9
- package/src/router.ts +114 -24
package/dist/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web-react/query`
|
|
2
|
+
* `@nifrajs/web-react/query` - React bindings for the keyed query-cache + mutations: `useQuery`,
|
|
3
3
|
* `useInfiniteQuery`, `useMutation`, `useQueryClient`, `QueryClientProvider`, and the SSR
|
|
4
4
|
* `HydrationBoundary` (+ `dehydrate` re-exported). A drop-in for the TanStack Query surface,
|
|
5
5
|
* backed by `@nifrajs/web`'s agnostic engine. Imports only `react` + `@nifrajs/web` (never `react-dom/*`),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Resolution order for the client a hook uses: a `QueryClientProvider` in the tree (required for SSR
|
|
9
9
|
* dehydrate/hydrate and for tests), else a lazily-created **client-side** module singleton (the simple
|
|
10
|
-
* client-only app
|
|
10
|
+
* client-only app - the `typeof window` guard means the server has none, so hooks render idle/pending
|
|
11
11
|
* and the first client render matches for a clean hydration).
|
|
12
12
|
*/
|
|
13
13
|
import { createMutation, createQueryClient, } from "@nifrajs/web";
|
|
@@ -35,7 +35,7 @@ const NOOP_CLIENT = {
|
|
|
35
35
|
hydrate: () => { },
|
|
36
36
|
};
|
|
37
37
|
const QueryClientContext = createContext(undefined);
|
|
38
|
-
/** Provide a {@link QueryClient} to the tree
|
|
38
|
+
/** Provide a {@link QueryClient} to the tree - required for SSR dehydrate/hydrate and for tests; a
|
|
39
39
|
* client-only app can omit it and rely on the built-in client-side singleton. */
|
|
40
40
|
export function QueryClientProvider(props) {
|
|
41
41
|
return createElement(QueryClientContext.Provider, { value: props.client }, props.children);
|
|
@@ -91,7 +91,7 @@ export function useQuery(key, fn, options) {
|
|
|
91
91
|
// A disabled query still binds a handle (so it re-renders when re-enabled), but never fetches.
|
|
92
92
|
const handle = client.query(key, fn, queryOpts);
|
|
93
93
|
// Server snapshot = the handle's own snapshot: without a provider the client is the NOOP one (idle),
|
|
94
|
-
// but WITH a hydrated provider client it returns the server-seeded data
|
|
94
|
+
// but WITH a hydrated provider client it returns the server-seeded data - so a HydrationBoundary-fed
|
|
95
95
|
// query renders its data during SSR and the first client render matches (no loading flash, no drift).
|
|
96
96
|
const state = useSyncExternalStore(handle.subscribe, handle.snapshot, handle.snapshot);
|
|
97
97
|
useEffect(() => {
|
|
@@ -159,7 +159,7 @@ export function useInfiniteQuery(key, fn, options) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
|
-
* Seed the context's {@link QueryClient} from a server {@link dehydrate} snapshot
|
|
162
|
+
* Seed the context's {@link QueryClient} from a server {@link dehydrate} snapshot - the SSR data bridge.
|
|
163
163
|
* Wrap the app (inside `QueryClientProvider`) so server-prefetched queries are in the cache before the
|
|
164
164
|
* first client render, avoiding a loading flash. Hydration runs during render (idempotent, fresher-wins),
|
|
165
165
|
* so the data is available synchronously to child `useQuery`s.
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve `react-dom/server` so it shares ONE React core with the route components
|
|
2
|
+
* Resolve `react-dom/server` so it shares ONE React core with the route components - the fix for the
|
|
3
3
|
* dual-React SSR crash (`resolveDispatcher().useState` is null / "Invalid hook call: mismatching versions
|
|
4
4
|
* of React and the renderer").
|
|
5
5
|
*
|
|
6
6
|
* WHY this exists: under Bun **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process),
|
|
7
7
|
* a static `import "react-dom/server"` in this adapter is resolved by Bun from THIS package's own
|
|
8
|
-
* (symlinked) node_modules
|
|
8
|
+
* (symlinked) node_modules - which can be a DIFFERENT physical `react` than the one the consumer app's
|
|
9
9
|
* route components import. Two React cores → two hook dispatchers → the renderer's dispatcher is the wrong
|
|
10
10
|
* (or null) one → the crash. Resolving `react-dom/server` from the consumer **app root** instead makes
|
|
11
11
|
* react-dom pull the app's `react` transitively, matching the components' `react`: a single core, a single
|
|
12
|
-
* dispatcher, no crash. (Empirically verified against a two-copy install fixture
|
|
12
|
+
* dispatcher, no crash. (Empirically verified against a two-copy install fixture - see
|
|
13
13
|
* test/dual-react.test.ts.)
|
|
14
14
|
*
|
|
15
|
-
* Guarding precisely so the BUILT path is untouched: a bundle is detected two ways
|
|
15
|
+
* Guarding precisely so the BUILT path is untouched: a bundle is detected two ways - `Bun.resolveSync` is
|
|
16
16
|
* unavailable (Node / Deno / Cloudflare / Vercel), OR `buildServer` tagged the output with
|
|
17
17
|
* `process.env.NIFRA_SSR_BUNDLED` (a `target:"bun"` bundle DOES keep `Bun.resolveSync` under the Bun
|
|
18
18
|
* runtime, so the resolver test alone can't see it). In either case the build already bundled+deduped a
|
|
19
19
|
* single `react-dom` (buildServer's `reactDedupePlugin` pins `react`), so the static `import` is correct.
|
|
20
|
-
* Re-rooting a bundle would instead re-import a SECOND react-dom from disk
|
|
20
|
+
* Re-rooting a bundle would instead re-import a SECOND react-dom from disk - a second React core whose hook
|
|
21
21
|
* dispatcher is null for the bundled components → the `…H.useRef of null` SSR crash. The app-root re-root
|
|
22
22
|
* therefore runs ONLY under an UNBUNDLED Bun runtime (nifra dev/start, nifra_render), where the duplication
|
|
23
23
|
* can occur, `Bun.resolveSync` exists, and no bundle marker is present.
|
|
24
24
|
*/
|
|
25
25
|
import type { ReactNode } from "react";
|
|
26
26
|
/** The slice of `react-dom/server` this adapter uses. Typed locally so the dynamic import (which Bun
|
|
27
|
-
* resolves to an absolute path string) stays strict
|
|
27
|
+
* resolves to an absolute path string) stays strict - no `any` crosses the boundary. */
|
|
28
28
|
export interface ReactDomServer {
|
|
29
29
|
renderToString(node: ReactNode): string;
|
|
30
30
|
renderToReadableStream(node: ReactNode): Promise<ReadableStream<Uint8Array>>;
|
|
@@ -66,7 +66,7 @@ export declare function assertSingleReactCore(reactDomServerPath: string, resolv
|
|
|
66
66
|
*/
|
|
67
67
|
export declare function loadReactDomServer(resolve?: ResolveSync | undefined): Promise<ReactDomServer>;
|
|
68
68
|
/**
|
|
69
|
-
* The resolver `loadReactDomServer` uses by default, or `undefined` when re-rooting must NOT happen
|
|
69
|
+
* The resolver `loadReactDomServer` uses by default, or `undefined` when re-rooting must NOT happen - a
|
|
70
70
|
* non-Bun host (no `Bun.resolveSync`; the static import is the only path) OR a BUNDLED SSR output.
|
|
71
71
|
* `buildServer` defines `process.env.NIFRA_SSR_BUNDLED` to `"1"` in every bundle, where react-dom is
|
|
72
72
|
* already inlined + deduped to the components' React (reactDedupePlugin); re-rooting there would re-import
|
package/dist/react-dom-server.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve `react-dom/server` so it shares ONE React core with the route components
|
|
2
|
+
* Resolve `react-dom/server` so it shares ONE React core with the route components - the fix for the
|
|
3
3
|
* dual-React SSR crash (`resolveDispatcher().useState` is null / "Invalid hook call: mismatching versions
|
|
4
4
|
* of React and the renderer").
|
|
5
5
|
*
|
|
6
6
|
* WHY this exists: under Bun **runtime** SSR (`nifra dev`, `nifra start`, `nifra_render`, all in-process),
|
|
7
7
|
* a static `import "react-dom/server"` in this adapter is resolved by Bun from THIS package's own
|
|
8
|
-
* (symlinked) node_modules
|
|
8
|
+
* (symlinked) node_modules - which can be a DIFFERENT physical `react` than the one the consumer app's
|
|
9
9
|
* route components import. Two React cores → two hook dispatchers → the renderer's dispatcher is the wrong
|
|
10
10
|
* (or null) one → the crash. Resolving `react-dom/server` from the consumer **app root** instead makes
|
|
11
11
|
* react-dom pull the app's `react` transitively, matching the components' `react`: a single core, a single
|
|
12
|
-
* dispatcher, no crash. (Empirically verified against a two-copy install fixture
|
|
12
|
+
* dispatcher, no crash. (Empirically verified against a two-copy install fixture - see
|
|
13
13
|
* test/dual-react.test.ts.)
|
|
14
14
|
*
|
|
15
|
-
* Guarding precisely so the BUILT path is untouched: a bundle is detected two ways
|
|
15
|
+
* Guarding precisely so the BUILT path is untouched: a bundle is detected two ways - `Bun.resolveSync` is
|
|
16
16
|
* unavailable (Node / Deno / Cloudflare / Vercel), OR `buildServer` tagged the output with
|
|
17
17
|
* `process.env.NIFRA_SSR_BUNDLED` (a `target:"bun"` bundle DOES keep `Bun.resolveSync` under the Bun
|
|
18
18
|
* runtime, so the resolver test alone can't see it). In either case the build already bundled+deduped a
|
|
19
19
|
* single `react-dom` (buildServer's `reactDedupePlugin` pins `react`), so the static `import` is correct.
|
|
20
|
-
* Re-rooting a bundle would instead re-import a SECOND react-dom from disk
|
|
20
|
+
* Re-rooting a bundle would instead re-import a SECOND react-dom from disk - a second React core whose hook
|
|
21
21
|
* dispatcher is null for the bundled components → the `…H.useRef of null` SSR crash. The app-root re-root
|
|
22
22
|
* therefore runs ONLY under an UNBUNDLED Bun runtime (nifra dev/start, nifra_render), where the duplication
|
|
23
23
|
* can occur, `Bun.resolveSync` exists, and no bundle marker is present.
|
|
@@ -120,7 +120,7 @@ export async function loadReactDomServer(resolve = bunResolverFn()) {
|
|
|
120
120
|
// Bun runtime SSR: re-root to the app's copy so react-dom shares the components' React. `react-dom`
|
|
121
121
|
// is a PEER dependency of this adapter, so a correct install puts it at the app root and this
|
|
122
122
|
// resolves. If it somehow doesn't (an unusual nested layout), fall through to the bundled specifier
|
|
123
|
-
// rather than crashing
|
|
123
|
+
// rather than crashing - degraded (possible duplicate) but never a hard failure.
|
|
124
124
|
try {
|
|
125
125
|
const resolved = resolve("react-dom/server", appRoot());
|
|
126
126
|
// Before returning the module, verify the react-dom we just re-rooted shares the components' React.
|
|
@@ -144,7 +144,7 @@ export async function loadReactDomServer(resolve = bunResolverFn()) {
|
|
|
144
144
|
return (await import("react-dom/server"));
|
|
145
145
|
}
|
|
146
146
|
/**
|
|
147
|
-
* The resolver `loadReactDomServer` uses by default, or `undefined` when re-rooting must NOT happen
|
|
147
|
+
* The resolver `loadReactDomServer` uses by default, or `undefined` when re-rooting must NOT happen - a
|
|
148
148
|
* non-Bun host (no `Bun.resolveSync`; the static import is the only path) OR a BUNDLED SSR output.
|
|
149
149
|
* `buildServer` defines `process.env.NIFRA_SSR_BUNDLED` to `"1"` in every bundle, where react-dom is
|
|
150
150
|
* already inlined + deduped to the components' React (reactDedupePlugin); re-rooting there would re-import
|
package/dist/router.d.ts
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web-react/router`
|
|
2
|
+
* `@nifrajs/web-react/router` - React routing bindings over the agnostic `@nifrajs/web` router:
|
|
3
3
|
* `<Link>`/`<NavLink>`, `useNavigate`, `useParams`, `useLocation`, `useSearchParams`, and `<Navigate>`.
|
|
4
4
|
*
|
|
5
5
|
* These read the current route from a {@link RouterContext} that `compose` provides on BOTH the SSR
|
|
6
6
|
* render and the client mount (seeded from the request match / router state respectively), so the read
|
|
7
7
|
* hooks (`useParams`/`useLocation`/`useSearchParams`) are SSR-correct and hydrate with no mismatch.
|
|
8
8
|
* Navigation goes through `@nifrajs/web`'s DOM-free navigate bridge (`getBrowserNavigate`, populated by
|
|
9
|
-
* `installHistory`)
|
|
9
|
+
* `installHistory`) - so this module imports only `react` (never `react-dom/*`), and a route component
|
|
10
10
|
* can use these on the server and the client without dragging a DOM build into the wrong bundle. No JSX
|
|
11
11
|
* (the package builds with plain `tsc`), so everything is `createElement`.
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import type { InferOutput, StandardSchemaV1 } from "@nifrajs/core/server";
|
|
14
|
+
import { type Blocker, type BlockerFunction, type NavigateFunction, type NavigateOptions } from "@nifrajs/web";
|
|
14
15
|
import { type AnchorHTMLAttributes, type CSSProperties, type ReactNode } from "react";
|
|
16
|
+
export type { Blocker, BlockerFunction, BlockerState, NavigateFunction } from "@nifrajs/web";
|
|
15
17
|
/** The current route the routing hooks read. Provided by `compose` on SSR + client mount alike. */
|
|
16
18
|
export interface RouterContextValue {
|
|
17
19
|
/** The matched route's decoded path params (`/users/:id` → `{ id: "7" }`). */
|
|
18
20
|
readonly params: Readonly<Record<string, string>>;
|
|
19
|
-
/** The current URL's `pathname + search` (no hash
|
|
21
|
+
/** The current URL's `pathname + search` (no hash - the router never carries one). */
|
|
20
22
|
readonly path: string;
|
|
21
|
-
/**
|
|
23
|
+
/** The route's typed, validated search params (the loader's `ctx.search`), derived from the URL via
|
|
24
|
+
* the shared `searchOf` on SSR + client alike, read by {@link useSearch}. `{}` when the route
|
|
25
|
+
* declares no `searchSchema` (then it is the raw parsed query) or outside a nifra route tree. */
|
|
26
|
+
readonly search: Record<string, unknown>;
|
|
27
|
+
/** True while a client navigation (or revalidation) is in flight - the current route stays mounted
|
|
22
28
|
* until the new one is ready. Always `false` on SSR (loaders block before render). Drives loading UI
|
|
23
29
|
* via {@link useNavigation}. */
|
|
24
30
|
readonly pending: boolean;
|
|
@@ -26,16 +32,33 @@ export interface RouterContextValue {
|
|
|
26
32
|
* during a same-route revalidation). Powers {@link NavLink}'s per-link `isPending`. */
|
|
27
33
|
readonly pendingPath?: string | undefined;
|
|
28
34
|
}
|
|
29
|
-
/** Router context. The default ({} params, "" path) is what a component sees when rendered
|
|
30
|
-
* nifra route tree
|
|
35
|
+
/** Router context. The default ({} params, "" path, {} search) is what a component sees when rendered
|
|
36
|
+
* outside a nifra route tree (the hooks stay defined, no throw, so a stray `useParams` degrades
|
|
37
|
+
* gracefully). */
|
|
31
38
|
export declare const RouterContext: import("react").Context<RouterContextValue>;
|
|
32
39
|
/**
|
|
33
|
-
* The matched route's decoded path params
|
|
40
|
+
* The matched route's decoded path params - `/users/:id` on `/users/7` → `{ id: "7" }`. SSR-correct:
|
|
34
41
|
* `compose` provides the same value server-side (from the request match) and client-side (from router
|
|
35
42
|
* state), so a param rendered into markup doesn't flash on hydration.
|
|
36
43
|
*/
|
|
37
44
|
export declare function useParams<T extends Record<string, string | undefined> = Record<string, string>>(): Readonly<T>;
|
|
38
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The route's typed, validated search params: the SAME value the loader received as `ctx.search`.
|
|
47
|
+
* SSR-correct: `compose` provides it from `searchOf(searchSchema, url.search)` server-side and from the
|
|
48
|
+
* identical derivation on the client mount, so a value rendered from it doesn't flash on hydration.
|
|
49
|
+
* Hostile input already failed closed to the schema's defaults at match time, so a component never
|
|
50
|
+
* parses `window.location.search` or guards against a bad query.
|
|
51
|
+
*
|
|
52
|
+
* Pass the route's `searchSchema` as the type argument to get its output type; bare, it's the raw parsed
|
|
53
|
+
* query (`Record<string, unknown>`), which is also what a route without a `searchSchema` yields.
|
|
54
|
+
*
|
|
55
|
+
* ```tsx
|
|
56
|
+
* export const searchSchema = v.object({ page: v.optional(v.fallback(v.number(), 1), 1) })
|
|
57
|
+
* const { page } = useSearch<typeof searchSchema>() // page: number
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function useSearch<Schema extends StandardSchemaV1 | undefined = undefined>(): Schema extends StandardSchemaV1 ? InferOutput<Schema> : Record<string, unknown>;
|
|
61
|
+
/** The parsed current location. `hash` is always `""` - the fragment is client-only and never reaches
|
|
39
62
|
* the router state / server, so exposing a live hash would hydration-mismatch; read `window.location.hash`
|
|
40
63
|
* directly (in an effect) if you truly need it. */
|
|
41
64
|
export interface Location {
|
|
@@ -71,13 +94,38 @@ export interface Navigation {
|
|
|
71
94
|
export declare function useNavigation(): Navigation;
|
|
72
95
|
/** Convenience boolean form of {@link useNavigation}: `true` while a client navigation is in flight. */
|
|
73
96
|
export declare function usePending(): boolean;
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
/** Get the {@link NavigateFunction}. Stable across renders; resolves the browser navigate at call time
|
|
79
|
-
* (so it works as soon as `installHistory` has run, and no-ops before then / on the server). */
|
|
97
|
+
/** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object;
|
|
98
|
+
* a render-time navigate isn't valid - use {@link Navigate}, which navigates in an effect). Stable across
|
|
99
|
+
* renders; resolves the browser navigate at call time (so it works as soon as `installHistory` has run,
|
|
100
|
+
* and no-ops before then / on the server). */
|
|
80
101
|
export declare function useNavigate(): NavigateFunction;
|
|
102
|
+
/**
|
|
103
|
+
* Guard navigation away from a page with unsaved work, confirming with your OWN async UI. Mirrors
|
|
104
|
+
* react-router's `useBlocker`: pass a boolean (`useBlocker(isDirty)`) or a predicate
|
|
105
|
+
* `({ currentLocation, nextLocation }) => boolean`, and get back a {@link Blocker}. When a navigation
|
|
106
|
+
* (a `<Link>`/anchor click, `useNavigate`, or a browser back/forward) is intercepted, `blocker.state`
|
|
107
|
+
* becomes `"blocked"` and `proceed`/`reset` go live - render a dialog and call `proceed()` to continue
|
|
108
|
+
* or `reset()` to stay put. It also arms the browser's native "Leave site?" prompt on tab close / reload.
|
|
109
|
+
* Idle (never blocks, `proceed`/`reset` are `undefined`) on the server and before hydration, so it's
|
|
110
|
+
* SSR-safe and hydration-stable.
|
|
111
|
+
*
|
|
112
|
+
* ```tsx
|
|
113
|
+
* const blocker = useBlocker(form.isDirty)
|
|
114
|
+
* return (
|
|
115
|
+
* <>
|
|
116
|
+
* <MyForm />
|
|
117
|
+
* {blocker.state === "blocked" && (
|
|
118
|
+
* <ConfirmDialog
|
|
119
|
+
* onCancel={blocker.reset}
|
|
120
|
+
* onConfirm={blocker.proceed}
|
|
121
|
+
* message="Discard unsaved changes?"
|
|
122
|
+
* />
|
|
123
|
+
* )}
|
|
124
|
+
* </>
|
|
125
|
+
* )
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export declare function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker;
|
|
81
129
|
/** The value forms `setSearchParams` accepts. */
|
|
82
130
|
export type SearchParamsInit = URLSearchParams | Record<string, string | readonly string[]> | string;
|
|
83
131
|
/** Set the query string. Accepts a `URLSearchParams`, a record, a raw string, or an updater of the
|
|
@@ -99,7 +147,7 @@ export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
|
99
147
|
* A client-navigating anchor. Renders a real `<a href={to}>` (so it's a working link before hydration
|
|
100
148
|
* and for right-click / open-in-new-tab), and on a plain left-click navigates through the router
|
|
101
149
|
* instead of a full reload. Calling `navigate` + `preventDefault` here means `installHistory`'s
|
|
102
|
-
* document-level click handler sees `defaultPrevented` and stands down
|
|
150
|
+
* document-level click handler sees `defaultPrevented` and stands down - exactly one navigation.
|
|
103
151
|
*/
|
|
104
152
|
export declare const Link: import("react").ForwardRefExoticComponent<LinkProps & import("react").RefAttributes<HTMLAnchorElement>>;
|
|
105
153
|
/** The state a {@link NavLink}'s function-form `className`/`style`/`children` receive. */
|
|
@@ -110,7 +158,7 @@ export interface NavLinkRenderProps {
|
|
|
110
158
|
* `false` on SSR and when idle. Use it to show a per-link spinner during the transition. */
|
|
111
159
|
readonly isPending: boolean;
|
|
112
160
|
}
|
|
113
|
-
/** {@link NavLink} props
|
|
161
|
+
/** {@link NavLink} props - like {@link LinkProps}, but `className`/`style`/`children` may be functions
|
|
114
162
|
* of the active state, and `end`/`caseSensitive` tune matching. */
|
|
115
163
|
export interface NavLinkProps extends Omit<LinkProps, "className" | "style" | "children"> {
|
|
116
164
|
/** Match the full path exactly instead of as a prefix (use for `to="/"` so it isn't always active). */
|
|
@@ -134,7 +182,7 @@ export interface NavigateProps {
|
|
|
134
182
|
readonly replace?: boolean;
|
|
135
183
|
}
|
|
136
184
|
/**
|
|
137
|
-
* Declaratively navigate on mount
|
|
185
|
+
* Declaratively navigate on mount - the component analogue of `useNavigate` (e.g. a guard that renders
|
|
138
186
|
* `<Navigate to="/login" replace />`). Navigates in an effect, so it's a safe no-op during SSR (renders
|
|
139
187
|
* `null`); the redirect happens once on the client after hydration.
|
|
140
188
|
*/
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/dist/router.js
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web-react/router`
|
|
2
|
+
* `@nifrajs/web-react/router` - React routing bindings over the agnostic `@nifrajs/web` router:
|
|
3
3
|
* `<Link>`/`<NavLink>`, `useNavigate`, `useParams`, `useLocation`, `useSearchParams`, and `<Navigate>`.
|
|
4
4
|
*
|
|
5
5
|
* These read the current route from a {@link RouterContext} that `compose` provides on BOTH the SSR
|
|
6
6
|
* render and the client mount (seeded from the request match / router state respectively), so the read
|
|
7
7
|
* hooks (`useParams`/`useLocation`/`useSearchParams`) are SSR-correct and hydrate with no mismatch.
|
|
8
8
|
* Navigation goes through `@nifrajs/web`'s DOM-free navigate bridge (`getBrowserNavigate`, populated by
|
|
9
|
-
* `installHistory`)
|
|
9
|
+
* `installHistory`) - so this module imports only `react` (never `react-dom/*`), and a route component
|
|
10
10
|
* can use these on the server and the client without dragging a DOM build into the wrong bundle. No JSX
|
|
11
11
|
* (the package builds with plain `tsc`), so everything is `createElement`.
|
|
12
12
|
*/
|
|
13
|
-
import { getBrowserNavigate } from "@nifrajs/web";
|
|
14
|
-
import { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useMemo, } from "react";
|
|
15
|
-
// Frozen empty params so the default context value has a stable reference (no needless re-renders).
|
|
13
|
+
import { getBrowserNavigate, IDLE_BLOCKER, registerBlocker, resolveNavigate, } from "@nifrajs/web";
|
|
14
|
+
import { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
15
|
+
// Frozen empty params/search so the default context value has a stable reference (no needless re-renders).
|
|
16
16
|
const EMPTY_PARAMS = Object.freeze({});
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const EMPTY_SEARCH = Object.freeze({});
|
|
18
|
+
/** Router context. The default ({} params, "" path, {} search) is what a component sees when rendered
|
|
19
|
+
* outside a nifra route tree (the hooks stay defined, no throw, so a stray `useParams` degrades
|
|
20
|
+
* gracefully). */
|
|
19
21
|
export const RouterContext = createContext({
|
|
20
22
|
params: EMPTY_PARAMS,
|
|
21
23
|
path: "",
|
|
24
|
+
search: EMPTY_SEARCH,
|
|
22
25
|
pending: false,
|
|
23
26
|
});
|
|
24
27
|
/** Split a `pathname + search` into its parts. `search` keeps its leading `?` (like `location.search`);
|
|
@@ -30,13 +33,31 @@ function splitPath(path) {
|
|
|
30
33
|
: { pathname: path.slice(0, q), search: path.slice(q) };
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
33
|
-
* The matched route's decoded path params
|
|
36
|
+
* The matched route's decoded path params - `/users/:id` on `/users/7` → `{ id: "7" }`. SSR-correct:
|
|
34
37
|
* `compose` provides the same value server-side (from the request match) and client-side (from router
|
|
35
38
|
* state), so a param rendered into markup doesn't flash on hydration.
|
|
36
39
|
*/
|
|
37
40
|
export function useParams() {
|
|
38
41
|
return useContext(RouterContext).params;
|
|
39
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* The route's typed, validated search params: the SAME value the loader received as `ctx.search`.
|
|
45
|
+
* SSR-correct: `compose` provides it from `searchOf(searchSchema, url.search)` server-side and from the
|
|
46
|
+
* identical derivation on the client mount, so a value rendered from it doesn't flash on hydration.
|
|
47
|
+
* Hostile input already failed closed to the schema's defaults at match time, so a component never
|
|
48
|
+
* parses `window.location.search` or guards against a bad query.
|
|
49
|
+
*
|
|
50
|
+
* Pass the route's `searchSchema` as the type argument to get its output type; bare, it's the raw parsed
|
|
51
|
+
* query (`Record<string, unknown>`), which is also what a route without a `searchSchema` yields.
|
|
52
|
+
*
|
|
53
|
+
* ```tsx
|
|
54
|
+
* export const searchSchema = v.object({ page: v.optional(v.fallback(v.number(), 1), 1) })
|
|
55
|
+
* const { page } = useSearch<typeof searchSchema>() // page: number
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function useSearch() {
|
|
59
|
+
return useContext(RouterContext).search;
|
|
60
|
+
}
|
|
40
61
|
/** The current {@link Location} (`pathname`/`search`/`hash`), derived from the router context. */
|
|
41
62
|
export function useLocation() {
|
|
42
63
|
const { path } = useContext(RouterContext);
|
|
@@ -69,15 +90,59 @@ export function useNavigation() {
|
|
|
69
90
|
export function usePending() {
|
|
70
91
|
return useContext(RouterContext).pending;
|
|
71
92
|
}
|
|
72
|
-
/** Get the {@link NavigateFunction}
|
|
73
|
-
*
|
|
93
|
+
/** Get the {@link NavigateFunction} (a string path, a history delta, or a typed `{ to, search }` object;
|
|
94
|
+
* a render-time navigate isn't valid - use {@link Navigate}, which navigates in an effect). Stable across
|
|
95
|
+
* renders; resolves the browser navigate at call time (so it works as soon as `installHistory` has run,
|
|
96
|
+
* and no-ops before then / on the server). */
|
|
74
97
|
export function useNavigate() {
|
|
75
98
|
return useCallback((to, options) => {
|
|
76
99
|
const navigate = getBrowserNavigate();
|
|
77
|
-
if (navigate
|
|
78
|
-
|
|
100
|
+
if (navigate === undefined)
|
|
101
|
+
return;
|
|
102
|
+
const resolved = resolveNavigate(to, options);
|
|
103
|
+
navigate(resolved.to, resolved.options);
|
|
79
104
|
}, []);
|
|
80
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Guard navigation away from a page with unsaved work, confirming with your OWN async UI. Mirrors
|
|
108
|
+
* react-router's `useBlocker`: pass a boolean (`useBlocker(isDirty)`) or a predicate
|
|
109
|
+
* `({ currentLocation, nextLocation }) => boolean`, and get back a {@link Blocker}. When a navigation
|
|
110
|
+
* (a `<Link>`/anchor click, `useNavigate`, or a browser back/forward) is intercepted, `blocker.state`
|
|
111
|
+
* becomes `"blocked"` and `proceed`/`reset` go live - render a dialog and call `proceed()` to continue
|
|
112
|
+
* or `reset()` to stay put. It also arms the browser's native "Leave site?" prompt on tab close / reload.
|
|
113
|
+
* Idle (never blocks, `proceed`/`reset` are `undefined`) on the server and before hydration, so it's
|
|
114
|
+
* SSR-safe and hydration-stable.
|
|
115
|
+
*
|
|
116
|
+
* ```tsx
|
|
117
|
+
* const blocker = useBlocker(form.isDirty)
|
|
118
|
+
* return (
|
|
119
|
+
* <>
|
|
120
|
+
* <MyForm />
|
|
121
|
+
* {blocker.state === "blocked" && (
|
|
122
|
+
* <ConfirmDialog
|
|
123
|
+
* onCancel={blocker.reset}
|
|
124
|
+
* onConfirm={blocker.proceed}
|
|
125
|
+
* message="Discard unsaved changes?"
|
|
126
|
+
* />
|
|
127
|
+
* )}
|
|
128
|
+
* </>
|
|
129
|
+
* )
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function useBlocker(shouldBlock) {
|
|
133
|
+
const [blocker, setBlocker] = useState(IDLE_BLOCKER);
|
|
134
|
+
// Track the latest predicate in a ref (updated every render) so the single registration always sees
|
|
135
|
+
// current component state (the dirty flag) WITHOUT re-registering - re-registering mid-prompt drops it.
|
|
136
|
+
const shouldBlockRef = useRef(shouldBlock);
|
|
137
|
+
shouldBlockRef.current = shouldBlock;
|
|
138
|
+
// Register once on mount; the returned unregister is the effect cleanup. A boolean reads as itself, a
|
|
139
|
+
// function is called. A no-op before `installHistory` has run (blocker stays idle → native navigation).
|
|
140
|
+
useEffect(() => registerBlocker((args) => {
|
|
141
|
+
const current = shouldBlockRef.current;
|
|
142
|
+
return typeof current === "function" ? current(args) : current;
|
|
143
|
+
}, setBlocker), []);
|
|
144
|
+
return blocker;
|
|
145
|
+
}
|
|
81
146
|
function toSearchParams(init) {
|
|
82
147
|
if (init instanceof URLSearchParams)
|
|
83
148
|
return init;
|
|
@@ -109,7 +174,7 @@ export function useSearchParams() {
|
|
|
109
174
|
}, [navigate, pathname, search]);
|
|
110
175
|
return [searchParams, setSearchParams];
|
|
111
176
|
}
|
|
112
|
-
/** A left-click with no modifier and no new-tab target
|
|
177
|
+
/** A left-click with no modifier and no new-tab target - the only click a client router should
|
|
113
178
|
* intercept (Cmd/Ctrl/Shift/Alt or a `_blank` target means the user wants native behavior). */
|
|
114
179
|
function isPlainLeftClick(event, target) {
|
|
115
180
|
if (event.button !== 0)
|
|
@@ -122,7 +187,7 @@ function isPlainLeftClick(event, target) {
|
|
|
122
187
|
* A client-navigating anchor. Renders a real `<a href={to}>` (so it's a working link before hydration
|
|
123
188
|
* and for right-click / open-in-new-tab), and on a plain left-click navigates through the router
|
|
124
189
|
* instead of a full reload. Calling `navigate` + `preventDefault` here means `installHistory`'s
|
|
125
|
-
* document-level click handler sees `defaultPrevented` and stands down
|
|
190
|
+
* document-level click handler sees `defaultPrevented` and stands down - exactly one navigation.
|
|
126
191
|
*/
|
|
127
192
|
export const Link = forwardRef(function Link({ to, replace, onClick, target, ...rest }, ref) {
|
|
128
193
|
const handleClick = (event) => {
|
|
@@ -187,7 +252,7 @@ function matchesPrefix(current, target) {
|
|
|
187
252
|
return current.startsWith(target === "/" ? "/" : `${target}/`);
|
|
188
253
|
}
|
|
189
254
|
/**
|
|
190
|
-
* Declaratively navigate on mount
|
|
255
|
+
* Declaratively navigate on mount - the component analogue of `useNavigate` (e.g. a guard that renders
|
|
191
256
|
* `<Navigate to="/login" replace />`). Navigates in an effect, so it's a safe no-op during SSR (renders
|
|
192
257
|
* `null`); the redirect happens once on the client after hydration.
|
|
193
258
|
*/
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/web-react",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "React render adapter for @nifrajs/web
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "React render adapter for @nifrajs/web - SSR + hydration (Bun-native JSX, no Babel).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
7
7
|
"typescript",
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
"bun": "./src/content.ts",
|
|
66
66
|
"types": "./dist/content.d.ts",
|
|
67
67
|
"default": "./dist/content.js"
|
|
68
|
+
},
|
|
69
|
+
"./fn": {
|
|
70
|
+
"bun": "./src/fn.ts",
|
|
71
|
+
"types": "./dist/fn.d.ts",
|
|
72
|
+
"default": "./dist/fn.js"
|
|
68
73
|
}
|
|
69
74
|
},
|
|
70
75
|
"files": [
|
|
@@ -80,10 +85,10 @@
|
|
|
80
85
|
"access": "public"
|
|
81
86
|
},
|
|
82
87
|
"dependencies": {
|
|
83
|
-
"@nifrajs/core": "2.
|
|
84
|
-
"@nifrajs/i18n": "2.
|
|
85
|
-
"@nifrajs/image": "2.
|
|
86
|
-
"@nifrajs/web": "2.
|
|
88
|
+
"@nifrajs/core": "2.4.0",
|
|
89
|
+
"@nifrajs/i18n": "2.4.0",
|
|
90
|
+
"@nifrajs/image": "2.4.0",
|
|
91
|
+
"@nifrajs/web": "2.4.0"
|
|
87
92
|
},
|
|
88
93
|
"peerDependencies": {
|
|
89
94
|
"react": "^19.0.0",
|
package/src/await.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Deferred } from "@nifrajs/web"
|
|
2
2
|
/**
|
|
3
|
-
* `@nifrajs/web-react/await`
|
|
3
|
+
* `@nifrajs/web-react/await` - the `<Await>` primitive for deferred loader data (`defer()`). Isomorphic
|
|
4
4
|
* and imports only `react` + a type from `@nifrajs/web` (never `react-dom/server`), so route components
|
|
5
5
|
* can use it without dragging server code into the client bundle. No JSX (the package builds with
|
|
6
6
|
* plain `tsc`).
|
|
@@ -8,7 +8,7 @@ import type { Deferred } from "@nifrajs/web"
|
|
|
8
8
|
* The server streams the boundary: `use(promise)` suspends, React flushes the fallback then the
|
|
9
9
|
* resolved content mid-stream. On the client the registry promise is already settled (the core's
|
|
10
10
|
* streamed `__nifraResolve` script tagged it `fulfilled`), so `use()` returns the value synchronously
|
|
11
|
-
*
|
|
11
|
+
* - React hydrates the server-revealed boundary's content directly (no re-fetch, no fallback flash).
|
|
12
12
|
*/
|
|
13
13
|
import { Component, createElement, type ReactNode, Suspense, use } from "react"
|
|
14
14
|
|
package/src/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MountRouterOptions, type RenderProps, searchOfChain } from "@nifrajs/web"
|
|
2
2
|
/**
|
|
3
|
-
* @nifrajs/web-react/client
|
|
3
|
+
* @nifrajs/web-react/client - React client runtime. `hydrate` hydrates a single SSR'd route;
|
|
4
4
|
* `mountRouter` hydrates a stateful Router that subscribes to the agnostic store (via
|
|
5
5
|
* `useSyncExternalStore`) and re-renders the matched chain on every client navigation (no full
|
|
6
6
|
* reload). Kept in its own entry so server code stays out of the client bundle.
|
|
@@ -10,7 +10,7 @@ import { hydrateRoot } from "react-dom/client"
|
|
|
10
10
|
import { compose } from "./compose.ts"
|
|
11
11
|
import { setMountedRouter } from "./fetcher.ts"
|
|
12
12
|
|
|
13
|
-
// The `_error` boundary chain element
|
|
13
|
+
// The `_error` boundary chain element - defined in its own (react-dom-free) module, re-exported here so
|
|
14
14
|
// nifra's client codegen resolves it from `@nifrajs/web-react/client` alongside `mountRouter`.
|
|
15
15
|
export { errorBoundary } from "./error.ts"
|
|
16
16
|
|
|
@@ -21,25 +21,31 @@ export function hydrate(chain: readonly unknown[], props: RenderProps, container
|
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Hydrate a stateful React Router. `useSyncExternalStore` subscribes to the agnostic store and
|
|
24
|
-
* re-renders the matched layout chain on each store change
|
|
24
|
+
* re-renders the matched layout chain on each store change - so client navigations swap routes
|
|
25
25
|
* without a full reload. `getServerSnapshot` (3rd arg) returns the initial state, matching the
|
|
26
26
|
* SSR markup on hydration.
|
|
27
27
|
*/
|
|
28
28
|
export function mountRouter(options: MountRouterOptions): void {
|
|
29
|
-
const { router, routes, container } = options
|
|
29
|
+
const { router, routes, searchSchemas, container } = options
|
|
30
30
|
setMountedRouter(router) // expose it to useFetcher/useFetchers (same page, client-only)
|
|
31
31
|
const Router: FunctionComponent = () => {
|
|
32
32
|
const state = useSyncExternalStore(router.subscribe, router.snapshot, router.snapshot)
|
|
33
|
+
// Derive this route's typed `search` from the URL query + the route's schema CHAIN (layout schemas +
|
|
34
|
+
// page, merged page-wins), the SAME `searchOfChain` the server ran, so `useSearch` reads an identical
|
|
35
|
+
// value and hydrates with no drift.
|
|
36
|
+
const q = state.path.indexOf("?")
|
|
37
|
+
const rawSearch = q === -1 ? "" : state.path.slice(q)
|
|
33
38
|
return compose(routes[state.routeId] ?? [], {
|
|
34
39
|
data: state.data,
|
|
35
40
|
actionData: state.actionData,
|
|
36
41
|
pending: state.pending,
|
|
37
42
|
...(state.pendingPath !== undefined ? { pendingPath: state.pendingPath } : {}),
|
|
38
|
-
// `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext
|
|
43
|
+
// `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext -
|
|
39
44
|
// sourced from router state here, matching the SSR render's request-derived values on hydration.
|
|
40
45
|
params: state.params,
|
|
41
46
|
path: state.path,
|
|
42
|
-
|
|
47
|
+
search: searchOfChain(searchSchemas?.[state.routeId] ?? [], rawSearch),
|
|
48
|
+
// The in-flight submission (for optimistic UI) - spread only when present.
|
|
43
49
|
...(state.submission ? { submission: state.submission } : {}),
|
|
44
50
|
})
|
|
45
51
|
}
|