@nifrajs/web-react 1.13.0 → 2.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/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/compose.d.ts +5 -4
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +13 -5
- package/dist/compose.js.map +1 -1
- package/dist/router.d.ts +35 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +38 -2
- package/dist/router.js.map +1 -1
- package/package.json +5 -5
- package/src/client.ts +1 -0
- package/src/compose.ts +13 -5
- package/src/router.ts +66 -3
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAcnE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAE/F;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAcnE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,mGAAmG;AACnG,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,IAAI,CAE/F;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAmB7D"}
|
package/dist/client.js
CHANGED
|
@@ -30,6 +30,7 @@ export function mountRouter(options) {
|
|
|
30
30
|
data: state.data,
|
|
31
31
|
actionData: state.actionData,
|
|
32
32
|
pending: state.pending,
|
|
33
|
+
...(state.pendingPath !== undefined ? { pendingPath: state.pendingPath } : {}),
|
|
33
34
|
// `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext —
|
|
34
35
|
// sourced from router state here, matching the SSR render's request-derived values on hydration.
|
|
35
36
|
params: state.params,
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAA0B,oBAAoB,EAAE,MAAM,OAAO,CAAA;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,wGAAwG;AACxG,+FAA+F;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,mGAAmG;AACnG,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB,EAAE,SAAkB;IACvF,WAAW,CAAC,SAAoB,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAA;IAC7C,gBAAgB,CAAC,MAAM,CAAC,CAAA,CAAC,+DAA+D;IACxF,MAAM,MAAM,GAAsB,GAAG,EAAE;QACrC,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QACtF,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE;YAC1C,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,+FAA+F;YAC/F,iGAAiG;YACjG,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,2EAA2E;YAC3E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC,CAAA;IACJ,CAAC,CAAA;IACD,WAAW,CAAC,SAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;AAC1D,CAAC"}
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA;;;;;GAKG;AACH,OAAO,EAAE,aAAa,EAA0B,oBAAoB,EAAE,MAAM,OAAO,CAAA;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAE/C,wGAAwG;AACxG,+FAA+F;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,mGAAmG;AACnG,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB,EAAE,SAAkB;IACvF,WAAW,CAAC,SAAoB,EAAE,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAC1D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAA2B;IACrD,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAA;IAC7C,gBAAgB,CAAC,MAAM,CAAC,CAAA,CAAC,+DAA+D;IACxF,MAAM,MAAM,GAAsB,GAAG,EAAE;QACrC,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAA;QACtF,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE;YAC1C,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9E,+FAA+F;YAC/F,iGAAiG;YACjG,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,2EAA2E;YAC3E,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC,CAAA;IACJ,CAAC,CAAA;IACD,WAAW,CAAC,SAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;AAC1D,CAAC"}
|
package/dist/compose.d.ts
CHANGED
|
@@ -5,10 +5,11 @@ import { type ReactNode } from "react";
|
|
|
5
5
|
* (innermost) receives `props` (the loader data); each layout wraps the child via its
|
|
6
6
|
* `children`. Shared by the server adapter (renderToString) and the client (hydrateRoot).
|
|
7
7
|
*
|
|
8
|
-
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params
|
|
9
|
-
* current `path` (
|
|
10
|
-
* hooks (`useParams`/`useLocation`/`
|
|
11
|
-
*
|
|
8
|
+
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params`, the
|
|
9
|
+
* current `path`, and the client `pending` flag (all threaded through `RenderProps` identically on SSR
|
|
10
|
+
* and client), so the routing hooks (`useParams`/`useLocation`/`useNavigation`) read the same value on
|
|
11
|
+
* both sides - no hydration mismatch (`pending` is `false` on SSR and on the initial client render).
|
|
12
|
+
* A render with no routing fields (a non-router adapter usage) provides the empty default.
|
|
12
13
|
*/
|
|
13
14
|
export declare function compose(chain: readonly unknown[], props: RenderProps): ReactNode;
|
|
14
15
|
//# sourceMappingURL=compose.d.ts.map
|
package/dist/compose.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7E
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7E;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,CAmBhF"}
|
package/dist/compose.js
CHANGED
|
@@ -7,10 +7,11 @@ const EMPTY_PARAMS = Object.freeze({});
|
|
|
7
7
|
* (innermost) receives `props` (the loader data); each layout wraps the child via its
|
|
8
8
|
* `children`. Shared by the server adapter (renderToString) and the client (hydrateRoot).
|
|
9
9
|
*
|
|
10
|
-
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params
|
|
11
|
-
* current `path` (
|
|
12
|
-
* hooks (`useParams`/`useLocation`/`
|
|
13
|
-
*
|
|
10
|
+
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params`, the
|
|
11
|
+
* current `path`, and the client `pending` flag (all threaded through `RenderProps` identically on SSR
|
|
12
|
+
* and client), so the routing hooks (`useParams`/`useLocation`/`useNavigation`) read the same value on
|
|
13
|
+
* both sides - no hydration mismatch (`pending` is `false` on SSR and on the initial client render).
|
|
14
|
+
* A render with no routing fields (a non-router adapter usage) provides the empty default.
|
|
14
15
|
*/
|
|
15
16
|
export function compose(chain, props) {
|
|
16
17
|
const last = chain.length - 1;
|
|
@@ -19,6 +20,13 @@ export function compose(chain, props) {
|
|
|
19
20
|
// children passed as the 3rd arg (not a `children` prop) — React's canonical form.
|
|
20
21
|
node = createElement(chain[i], null, node);
|
|
21
22
|
}
|
|
22
|
-
return createElement(RouterContext.Provider, {
|
|
23
|
+
return createElement(RouterContext.Provider, {
|
|
24
|
+
value: {
|
|
25
|
+
params: props.params ?? EMPTY_PARAMS,
|
|
26
|
+
path: props.path ?? "",
|
|
27
|
+
pending: props.pending ?? false,
|
|
28
|
+
pendingPath: props.pendingPath,
|
|
29
|
+
},
|
|
30
|
+
}, node);
|
|
23
31
|
}
|
|
24
32
|
//# sourceMappingURL=compose.js.map
|
package/dist/compose.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA0C,MAAM,OAAO,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,oGAAoG;AACpG,MAAM,YAAY,GAAqC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAExE
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAA0C,MAAM,OAAO,CAAA;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAE3C,oGAAoG;AACpG,MAAM,YAAY,GAAqC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;AAExE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,OAAO,CAAC,KAAyB,EAAE,KAAkB;IACnE,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IAC7B,IAAI,IAAI,GAAc,aAAa,CAAC,KAAK,CAAC,IAAI,CAAmC,EAAE,KAAK,CAAC,CAAA;IACzF,KAAK,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,mFAAmF;QACnF,IAAI,GAAG,aAAa,CAAC,KAAK,CAAC,CAAC,CAAsB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;IACjE,CAAC;IACD,OAAO,aAAa,CAClB,aAAa,CAAC,QAAQ,EACtB;QACE,KAAK,EAAE;YACL,MAAM,EAAE,KAAK,CAAC,MAAM,IAAI,YAAY;YACpC,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,EAAE;YACtB,OAAO,EAAE,KAAK,CAAC,OAAO,IAAI,KAAK;YAC/B,WAAW,EAAE,KAAK,CAAC,WAAW;SAC/B;KACF,EACD,IAAI,CACL,CAAA;AACH,CAAC"}
|
package/dist/router.d.ts
CHANGED
|
@@ -18,6 +18,13 @@ export interface RouterContextValue {
|
|
|
18
18
|
readonly params: Readonly<Record<string, string>>;
|
|
19
19
|
/** The current URL's `pathname + search` (no hash — the router never carries one). */
|
|
20
20
|
readonly path: string;
|
|
21
|
+
/** True while a client navigation (or revalidation) is in flight — the current route stays mounted
|
|
22
|
+
* until the new one is ready. Always `false` on SSR (loaders block before render). Drives loading UI
|
|
23
|
+
* via {@link useNavigation}. */
|
|
24
|
+
readonly pending: boolean;
|
|
25
|
+
/** The `pathname + search` a navigation is transitioning TO while `pending` (`undefined` when idle or
|
|
26
|
+
* during a same-route revalidation). Powers {@link NavLink}'s per-link `isPending`. */
|
|
27
|
+
readonly pendingPath?: string | undefined;
|
|
21
28
|
}
|
|
22
29
|
/** Router context. The default ({} params, "" path) is what a component sees when rendered outside a
|
|
23
30
|
* nifra route tree — the hooks stay defined (no throw) so a stray `useParams` degrades gracefully. */
|
|
@@ -38,6 +45,32 @@ export interface Location {
|
|
|
38
45
|
}
|
|
39
46
|
/** The current {@link Location} (`pathname`/`search`/`hash`), derived from the router context. */
|
|
40
47
|
export declare function useLocation(): Location;
|
|
48
|
+
/** The current navigation state, mirroring the Remix `useNavigation()` shape for familiarity. */
|
|
49
|
+
export interface Navigation {
|
|
50
|
+
/** True while a client navigation (or revalidation) is in flight. The current route stays mounted
|
|
51
|
+
* until the new one is ready, so this drives a loading indicator, not a route swap. */
|
|
52
|
+
readonly pending: boolean;
|
|
53
|
+
/** `"loading"` while a navigation is in flight, else `"idle"`. */
|
|
54
|
+
readonly state: "idle" | "loading";
|
|
55
|
+
/** The `pathname + search` being navigated TO while `pending`; `undefined` when idle or during a
|
|
56
|
+
* same-route revalidation. Mirrors Remix's `useNavigation().location`. */
|
|
57
|
+
readonly location: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Observe client navigation to drive loading UI (a top-bar spinner, dimmed content, a skeleton). nifra
|
|
61
|
+
* navigates imperatively - it fetches the next route's chunk + loader data while the current route stays
|
|
62
|
+
* on screen, then swaps - so `pending` is the signal for "a transition is in flight," not a Suspense
|
|
63
|
+
* boundary. Always `{ pending: false, state: "idle" }` on the server (loaders block before render), so
|
|
64
|
+
* it is hydration-safe.
|
|
65
|
+
*
|
|
66
|
+
* ```tsx
|
|
67
|
+
* const { pending } = useNavigation()
|
|
68
|
+
* return <div className={pending ? "opacity-60" : ""}>{pending && <TopBarSpinner />}<Outlet /></div>
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export declare function useNavigation(): Navigation;
|
|
72
|
+
/** Convenience boolean form of {@link useNavigation}: `true` while a client navigation is in flight. */
|
|
73
|
+
export declare function usePending(): boolean;
|
|
41
74
|
/** A programmatic navigate: a string path (push, or replace via `{ replace: true }`) or a history delta
|
|
42
75
|
* (`-1`/`1`). A no-op on the server / before hydration (a render-time navigate isn't valid — use
|
|
43
76
|
* {@link Navigate}, which navigates in an effect). */
|
|
@@ -73,7 +106,8 @@ export declare const Link: import("react").ForwardRefExoticComponent<LinkProps &
|
|
|
73
106
|
export interface NavLinkRenderProps {
|
|
74
107
|
/** True when the current location matches this link's `to` (prefix match, or exact when `end`). */
|
|
75
108
|
readonly isActive: boolean;
|
|
76
|
-
/**
|
|
109
|
+
/** True while a client navigation to THIS link's target is in flight (matched like `isActive`).
|
|
110
|
+
* `false` on SSR and when idle. Use it to show a per-link spinner during the transition. */
|
|
77
111
|
readonly isPending: boolean;
|
|
78
112
|
}
|
|
79
113
|
/** {@link NavLink} props — like {@link LinkProps}, but `className`/`style`/`children` may be functions
|
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;AACH,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACvE,OAAO,EACL,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAKlB,KAAK,SAAS,EAKf,MAAM,OAAO,CAAA;AAEd,mGAAmG;AACnG,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACjD,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"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"}
|
package/dist/router.js
CHANGED
|
@@ -16,7 +16,11 @@ import { createContext, createElement, forwardRef, useCallback, useContext, useE
|
|
|
16
16
|
const EMPTY_PARAMS = Object.freeze({});
|
|
17
17
|
/** Router context. The default ({} params, "" path) is what a component sees when rendered outside a
|
|
18
18
|
* nifra route tree — the hooks stay defined (no throw) so a stray `useParams` degrades gracefully. */
|
|
19
|
-
export const RouterContext = createContext({
|
|
19
|
+
export const RouterContext = createContext({
|
|
20
|
+
params: EMPTY_PARAMS,
|
|
21
|
+
path: "",
|
|
22
|
+
pending: false,
|
|
23
|
+
});
|
|
20
24
|
/** Split a `pathname + search` into its parts. `search` keeps its leading `?` (like `location.search`);
|
|
21
25
|
* an empty query yields `""`. */
|
|
22
26
|
function splitPath(path) {
|
|
@@ -41,6 +45,30 @@ export function useLocation() {
|
|
|
41
45
|
return { pathname, search, hash: "" };
|
|
42
46
|
}, [path]);
|
|
43
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Observe client navigation to drive loading UI (a top-bar spinner, dimmed content, a skeleton). nifra
|
|
50
|
+
* navigates imperatively - it fetches the next route's chunk + loader data while the current route stays
|
|
51
|
+
* on screen, then swaps - so `pending` is the signal for "a transition is in flight," not a Suspense
|
|
52
|
+
* boundary. Always `{ pending: false, state: "idle" }` on the server (loaders block before render), so
|
|
53
|
+
* it is hydration-safe.
|
|
54
|
+
*
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { pending } = useNavigation()
|
|
57
|
+
* return <div className={pending ? "opacity-60" : ""}>{pending && <TopBarSpinner />}<Outlet /></div>
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function useNavigation() {
|
|
61
|
+
const { pending, pendingPath } = useContext(RouterContext);
|
|
62
|
+
return useMemo(() => ({
|
|
63
|
+
pending,
|
|
64
|
+
state: pending ? "loading" : "idle",
|
|
65
|
+
location: pending ? pendingPath : undefined,
|
|
66
|
+
}), [pending, pendingPath]);
|
|
67
|
+
}
|
|
68
|
+
/** Convenience boolean form of {@link useNavigation}: `true` while a client navigation is in flight. */
|
|
69
|
+
export function usePending() {
|
|
70
|
+
return useContext(RouterContext).pending;
|
|
71
|
+
}
|
|
44
72
|
/** Get the {@link NavigateFunction}. Stable across renders; resolves the browser navigate at call time
|
|
45
73
|
* (so it works as soon as `installHistory` has run, and no-ops before then / on the server). */
|
|
46
74
|
export function useNavigate() {
|
|
@@ -125,11 +153,19 @@ function normalizePath(pathname, caseSensitive) {
|
|
|
125
153
|
*/
|
|
126
154
|
export const NavLink = forwardRef(function NavLink({ to, end, caseSensitive, className, style, children, ...rest }, ref) {
|
|
127
155
|
const { pathname } = useLocation();
|
|
156
|
+
const { pending, pendingPath } = useContext(RouterContext);
|
|
128
157
|
const cs = caseSensitive === true;
|
|
129
158
|
const current = normalizePath(pathname, cs);
|
|
130
159
|
const target = normalizePath(splitPath(to).pathname, cs);
|
|
131
160
|
const isActive = end === true ? current === target : matchesPrefix(current, target);
|
|
132
|
-
|
|
161
|
+
// isPending: a navigation is in flight AND it's heading to THIS link's target (same match rule as
|
|
162
|
+
// isActive). `false` on SSR and idle (no pendingPath).
|
|
163
|
+
const pendingTarget = pending && pendingPath !== undefined
|
|
164
|
+
? normalizePath(splitPath(pendingPath).pathname, cs)
|
|
165
|
+
: undefined;
|
|
166
|
+
const isPending = pendingTarget !== undefined &&
|
|
167
|
+
(end === true ? pendingTarget === target : matchesPrefix(pendingTarget, target));
|
|
168
|
+
const renderProps = { isActive, isPending };
|
|
133
169
|
const resolvedClassName = typeof className === "function" ? className(renderProps) : className;
|
|
134
170
|
const resolvedStyle = typeof style === "function" ? style(renderProps) : style;
|
|
135
171
|
const resolvedChildren = typeof children === "function" ? children(renderProps) : children;
|
package/dist/router.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAwB,MAAM,cAAc,CAAA;AACvE,OAAO,EAGL,aAAa,EACb,aAAa,EACb,UAAU,EAGV,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,GACR,MAAM,OAAO,CAAA;
|
|
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
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "React render adapter for @nifrajs/web — SSR + hydration (Bun-native JSX, no Babel).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -80,10 +80,10 @@
|
|
|
80
80
|
"access": "public"
|
|
81
81
|
},
|
|
82
82
|
"dependencies": {
|
|
83
|
-
"@nifrajs/core": "
|
|
84
|
-
"@nifrajs/i18n": "
|
|
85
|
-
"@nifrajs/image": "
|
|
86
|
-
"@nifrajs/web": "
|
|
83
|
+
"@nifrajs/core": "2.0.0",
|
|
84
|
+
"@nifrajs/i18n": "2.0.0",
|
|
85
|
+
"@nifrajs/image": "2.0.0",
|
|
86
|
+
"@nifrajs/web": "2.0.0"
|
|
87
87
|
},
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"react": "^19.0.0",
|
package/src/client.ts
CHANGED
|
@@ -34,6 +34,7 @@ export function mountRouter(options: MountRouterOptions): void {
|
|
|
34
34
|
data: state.data,
|
|
35
35
|
actionData: state.actionData,
|
|
36
36
|
pending: state.pending,
|
|
37
|
+
...(state.pendingPath !== undefined ? { pendingPath: state.pendingPath } : {}),
|
|
37
38
|
// `params`/`path` feed the routing hooks (useParams/useLocation) via compose's RouterContext —
|
|
38
39
|
// sourced from router state here, matching the SSR render's request-derived values on hydration.
|
|
39
40
|
params: state.params,
|
package/src/compose.ts
CHANGED
|
@@ -10,10 +10,11 @@ const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({})
|
|
|
10
10
|
* (innermost) receives `props` (the loader data); each layout wraps the child via its
|
|
11
11
|
* `children`. Shared by the server adapter (renderToString) and the client (hydrateRoot).
|
|
12
12
|
*
|
|
13
|
-
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params
|
|
14
|
-
* current `path` (
|
|
15
|
-
* hooks (`useParams`/`useLocation`/`
|
|
16
|
-
*
|
|
13
|
+
* The whole tree is wrapped in a {@link RouterContext} provider carrying the matched `params`, the
|
|
14
|
+
* current `path`, and the client `pending` flag (all threaded through `RenderProps` identically on SSR
|
|
15
|
+
* and client), so the routing hooks (`useParams`/`useLocation`/`useNavigation`) read the same value on
|
|
16
|
+
* both sides - no hydration mismatch (`pending` is `false` on SSR and on the initial client render).
|
|
17
|
+
* A render with no routing fields (a non-router adapter usage) provides the empty default.
|
|
17
18
|
*/
|
|
18
19
|
export function compose(chain: readonly unknown[], props: RenderProps): ReactNode {
|
|
19
20
|
const last = chain.length - 1
|
|
@@ -24,7 +25,14 @@ export function compose(chain: readonly unknown[], props: RenderProps): ReactNod
|
|
|
24
25
|
}
|
|
25
26
|
return createElement(
|
|
26
27
|
RouterContext.Provider,
|
|
27
|
-
{
|
|
28
|
+
{
|
|
29
|
+
value: {
|
|
30
|
+
params: props.params ?? EMPTY_PARAMS,
|
|
31
|
+
path: props.path ?? "",
|
|
32
|
+
pending: props.pending ?? false,
|
|
33
|
+
pendingPath: props.pendingPath,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
28
36
|
node,
|
|
29
37
|
)
|
|
30
38
|
}
|
package/src/router.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface RouterContextValue {
|
|
|
31
31
|
readonly params: Readonly<Record<string, string>>
|
|
32
32
|
/** The current URL's `pathname + search` (no hash — the router never carries one). */
|
|
33
33
|
readonly path: string
|
|
34
|
+
/** True while a client navigation (or revalidation) is in flight — the current route stays mounted
|
|
35
|
+
* until the new one is ready. Always `false` on SSR (loaders block before render). Drives loading UI
|
|
36
|
+
* via {@link useNavigation}. */
|
|
37
|
+
readonly pending: boolean
|
|
38
|
+
/** The `pathname + search` a navigation is transitioning TO while `pending` (`undefined` when idle or
|
|
39
|
+
* during a same-route revalidation). Powers {@link NavLink}'s per-link `isPending`. */
|
|
40
|
+
readonly pendingPath?: string | undefined
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
// Frozen empty params so the default context value has a stable reference (no needless re-renders).
|
|
@@ -38,7 +45,11 @@ const EMPTY_PARAMS: Readonly<Record<string, string>> = Object.freeze({})
|
|
|
38
45
|
|
|
39
46
|
/** Router context. The default ({} params, "" path) is what a component sees when rendered outside a
|
|
40
47
|
* nifra route tree — the hooks stay defined (no throw) so a stray `useParams` degrades gracefully. */
|
|
41
|
-
export const RouterContext = createContext<RouterContextValue>({
|
|
48
|
+
export const RouterContext = createContext<RouterContextValue>({
|
|
49
|
+
params: EMPTY_PARAMS,
|
|
50
|
+
path: "",
|
|
51
|
+
pending: false,
|
|
52
|
+
})
|
|
42
53
|
|
|
43
54
|
/** Split a `pathname + search` into its parts. `search` keeps its leading `?` (like `location.search`);
|
|
44
55
|
* an empty query yields `""`. */
|
|
@@ -78,6 +89,47 @@ export function useLocation(): Location {
|
|
|
78
89
|
}, [path])
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/** The current navigation state, mirroring the Remix `useNavigation()` shape for familiarity. */
|
|
93
|
+
export interface Navigation {
|
|
94
|
+
/** True while a client navigation (or revalidation) is in flight. The current route stays mounted
|
|
95
|
+
* until the new one is ready, so this drives a loading indicator, not a route swap. */
|
|
96
|
+
readonly pending: boolean
|
|
97
|
+
/** `"loading"` while a navigation is in flight, else `"idle"`. */
|
|
98
|
+
readonly state: "idle" | "loading"
|
|
99
|
+
/** The `pathname + search` being navigated TO while `pending`; `undefined` when idle or during a
|
|
100
|
+
* same-route revalidation. Mirrors Remix's `useNavigation().location`. */
|
|
101
|
+
readonly location: string | undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Observe client navigation to drive loading UI (a top-bar spinner, dimmed content, a skeleton). nifra
|
|
106
|
+
* navigates imperatively - it fetches the next route's chunk + loader data while the current route stays
|
|
107
|
+
* on screen, then swaps - so `pending` is the signal for "a transition is in flight," not a Suspense
|
|
108
|
+
* boundary. Always `{ pending: false, state: "idle" }` on the server (loaders block before render), so
|
|
109
|
+
* it is hydration-safe.
|
|
110
|
+
*
|
|
111
|
+
* ```tsx
|
|
112
|
+
* const { pending } = useNavigation()
|
|
113
|
+
* return <div className={pending ? "opacity-60" : ""}>{pending && <TopBarSpinner />}<Outlet /></div>
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function useNavigation(): Navigation {
|
|
117
|
+
const { pending, pendingPath } = useContext(RouterContext)
|
|
118
|
+
return useMemo(
|
|
119
|
+
() => ({
|
|
120
|
+
pending,
|
|
121
|
+
state: pending ? ("loading" as const) : ("idle" as const),
|
|
122
|
+
location: pending ? pendingPath : undefined,
|
|
123
|
+
}),
|
|
124
|
+
[pending, pendingPath],
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Convenience boolean form of {@link useNavigation}: `true` while a client navigation is in flight. */
|
|
129
|
+
export function usePending(): boolean {
|
|
130
|
+
return useContext(RouterContext).pending
|
|
131
|
+
}
|
|
132
|
+
|
|
81
133
|
/** A programmatic navigate: a string path (push, or replace via `{ replace: true }`) or a history delta
|
|
82
134
|
* (`-1`/`1`). A no-op on the server / before hydration (a render-time navigate isn't valid — use
|
|
83
135
|
* {@link Navigate}, which navigates in an effect). */
|
|
@@ -175,7 +227,8 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
|
|
175
227
|
export interface NavLinkRenderProps {
|
|
176
228
|
/** True when the current location matches this link's `to` (prefix match, or exact when `end`). */
|
|
177
229
|
readonly isActive: boolean
|
|
178
|
-
/**
|
|
230
|
+
/** True while a client navigation to THIS link's target is in flight (matched like `isActive`).
|
|
231
|
+
* `false` on SSR and when idle. Use it to show a per-link spinner during the transition. */
|
|
179
232
|
readonly isPending: boolean
|
|
180
233
|
}
|
|
181
234
|
|
|
@@ -209,11 +262,21 @@ export const NavLink = forwardRef<HTMLAnchorElement, NavLinkProps>(function NavL
|
|
|
209
262
|
ref,
|
|
210
263
|
) {
|
|
211
264
|
const { pathname } = useLocation()
|
|
265
|
+
const { pending, pendingPath } = useContext(RouterContext)
|
|
212
266
|
const cs = caseSensitive === true
|
|
213
267
|
const current = normalizePath(pathname, cs)
|
|
214
268
|
const target = normalizePath(splitPath(to).pathname, cs)
|
|
215
269
|
const isActive = end === true ? current === target : matchesPrefix(current, target)
|
|
216
|
-
|
|
270
|
+
// isPending: a navigation is in flight AND it's heading to THIS link's target (same match rule as
|
|
271
|
+
// isActive). `false` on SSR and idle (no pendingPath).
|
|
272
|
+
const pendingTarget =
|
|
273
|
+
pending && pendingPath !== undefined
|
|
274
|
+
? normalizePath(splitPath(pendingPath).pathname, cs)
|
|
275
|
+
: undefined
|
|
276
|
+
const isPending =
|
|
277
|
+
pendingTarget !== undefined &&
|
|
278
|
+
(end === true ? pendingTarget === target : matchesPrefix(pendingTarget, target))
|
|
279
|
+
const renderProps: NavLinkRenderProps = { isActive, isPending }
|
|
217
280
|
const resolvedClassName = typeof className === "function" ? className(renderProps) : className
|
|
218
281
|
const resolvedStyle = typeof style === "function" ? style(renderProps) : style
|
|
219
282
|
const resolvedChildren = typeof children === "function" ? children(renderProps) : children
|