@real-router/react 0.27.2 → 0.27.4
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/cjs/{Link-CyDwbrFA.js → Link-D4K4T0Ef.js} +2 -2
- package/dist/cjs/{Link-CyDwbrFA.js.map → Link-D4K4T0Ef.js.map} +1 -1
- package/dist/cjs/{RouterErrorBoundary-3UhUelqY.d.ts → RouterErrorBoundary-k17kf8Rr.d.ts} +3 -3
- package/dist/cjs/{RouterErrorBoundary-3UhUelqY.d.ts.map → RouterErrorBoundary-k17kf8Rr.d.ts.map} +1 -1
- package/dist/cjs/{RouterProvider-CPsCmrRw.js → RouterProvider-CAaKExt_.js} +2 -2
- package/dist/cjs/RouterProvider-CAaKExt_.js.map +1 -0
- package/dist/cjs/{RouterProvider-lgNarto1.d.ts → RouterProvider-xbLo90sc.d.ts} +2 -2
- package/dist/cjs/{RouterProvider-lgNarto1.d.ts.map → RouterProvider-xbLo90sc.d.ts.map} +1 -1
- package/dist/cjs/index.d.ts +3 -3
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.react-server.d.ts +1 -1
- package/dist/cjs/ink.d.ts +2 -2
- package/dist/cjs/ink.js +1 -1
- package/dist/cjs/legacy.d.ts +3 -3
- package/dist/cjs/legacy.js +1 -1
- package/dist/cjs/{useRouterTransition-D5nwRENd.d.ts → useRouterTransition-CRFKfwuP.d.ts} +2 -2
- package/dist/cjs/{useRouterTransition-D5nwRENd.d.ts.map → useRouterTransition-CRFKfwuP.d.ts.map} +1 -1
- package/dist/esm/{Link-uERoM4yk.mjs → Link-PQIMnFd0.mjs} +2 -2
- package/dist/esm/{Link-uERoM4yk.mjs.map → Link-PQIMnFd0.mjs.map} +1 -1
- package/dist/esm/{RouterErrorBoundary-3UhUelqY.d.mts → RouterErrorBoundary-k17kf8Rr.d.mts} +3 -3
- package/dist/esm/{RouterErrorBoundary-3UhUelqY.d.mts.map → RouterErrorBoundary-k17kf8Rr.d.mts.map} +1 -1
- package/dist/esm/{RouterProvider-DCchvq4n.d.mts → RouterProvider-CG1cqrjE.d.mts} +2 -2
- package/dist/esm/{RouterProvider-DCchvq4n.d.mts.map → RouterProvider-CG1cqrjE.d.mts.map} +1 -1
- package/dist/esm/{RouterProvider-DgHypQTJ.mjs → RouterProvider-DhZoi6au.mjs} +2 -2
- package/dist/esm/RouterProvider-DhZoi6au.mjs.map +1 -0
- package/dist/esm/index.d.mts +3 -3
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.react-server.d.mts +1 -1
- package/dist/esm/ink.d.mts +2 -2
- package/dist/esm/ink.mjs +1 -1
- package/dist/esm/legacy.d.mts +3 -3
- package/dist/esm/legacy.mjs +1 -1
- package/dist/esm/{useRouterTransition-PCwA_qj1.d.mts → useRouterTransition-C5Ubt7rP.d.mts} +2 -2
- package/dist/esm/{useRouterTransition-PCwA_qj1.d.mts.map → useRouterTransition-C5Ubt7rP.d.mts.map} +1 -1
- package/package.json +6 -7
- package/dist/cjs/RouterProvider-CPsCmrRw.js.map +0 -1
- package/dist/esm/RouterProvider-DgHypQTJ.mjs.map +0 -1
- package/src/RouterProvider.tsx +0 -148
- package/src/components/Await.tsx +0 -46
- package/src/components/ClientOnly.tsx +0 -25
- package/src/components/HttpStatusCode.tsx +0 -66
- package/src/components/HttpStatusProvider.tsx +0 -26
- package/src/components/InkLink.tsx +0 -134
- package/src/components/InkRouterProvider.tsx +0 -9
- package/src/components/Link.tsx +0 -117
- package/src/components/RouterErrorBoundary.tsx +0 -69
- package/src/components/ServerOnly.tsx +0 -26
- package/src/components/Streamed.tsx +0 -30
- package/src/components/modern/RouteView/RouteView.tsx +0 -69
- package/src/components/modern/RouteView/components.tsx +0 -19
- package/src/components/modern/RouteView/helpers.tsx +0 -231
- package/src/components/modern/RouteView/index.ts +0 -8
- package/src/components/modern/RouteView/types.ts +0 -38
- package/src/constants.ts +0 -10
- package/src/context.ts +0 -14
- package/src/hooks/useDeferred.tsx +0 -34
- package/src/hooks/useIsActiveRoute.tsx +0 -47
- package/src/hooks/useNavigator.tsx +0 -15
- package/src/hooks/useRoute.tsx +0 -32
- package/src/hooks/useRouteEnter.tsx +0 -148
- package/src/hooks/useRouteExit.tsx +0 -159
- package/src/hooks/useRouteNode.tsx +0 -37
- package/src/hooks/useRouteUtils.tsx +0 -35
- package/src/hooks/useRouter.tsx +0 -15
- package/src/hooks/useRouterTransition.tsx +0 -17
- package/src/index.react-server.ts +0 -33
- package/src/index.ts +0 -61
- package/src/ink-types.ts +0 -25
- package/src/ink.ts +0 -30
- package/src/legacy.ssr.ts +0 -35
- package/src/legacy.ts +0 -35
- package/src/ssr.react-server.ts +0 -21
- package/src/ssr.ts +0 -43
- package/src/types.ts +0 -40
- package/src/utils/createHttpStatusSink.ts +0 -27
package/src/RouterProvider.tsx
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { getNavigator } from "@real-router/core";
|
|
2
|
-
import { createRouteSource } from "@real-router/sources";
|
|
3
|
-
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
4
|
-
|
|
5
|
-
import { NavigatorContext, RouteContext, RouterContext } from "./context";
|
|
6
|
-
import {
|
|
7
|
-
createRouteAnnouncer,
|
|
8
|
-
createScrollRestoration,
|
|
9
|
-
createScrollSpy,
|
|
10
|
-
createViewTransitions,
|
|
11
|
-
} from "./dom-utils";
|
|
12
|
-
|
|
13
|
-
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
14
|
-
import type { Router } from "@real-router/core";
|
|
15
|
-
import type { FC, ReactNode } from "react";
|
|
16
|
-
|
|
17
|
-
export interface RouteProviderProps {
|
|
18
|
-
router: Router;
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
announceNavigation?: boolean;
|
|
21
|
-
scrollRestoration?: ScrollRestorationOptions;
|
|
22
|
-
scrollSpy?: ScrollSpyOptions;
|
|
23
|
-
viewTransitions?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const RouterProvider: FC<RouteProviderProps> = ({
|
|
27
|
-
router,
|
|
28
|
-
children,
|
|
29
|
-
announceNavigation,
|
|
30
|
-
scrollRestoration,
|
|
31
|
-
scrollSpy,
|
|
32
|
-
viewTransitions,
|
|
33
|
-
}) => {
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
if (!announceNavigation) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const announcer = createRouteAnnouncer(router);
|
|
40
|
-
|
|
41
|
-
return () => {
|
|
42
|
-
announcer.destroy();
|
|
43
|
-
};
|
|
44
|
-
}, [announceNavigation, router]);
|
|
45
|
-
|
|
46
|
-
// Primitive deps so inline `{ mode: "restore" }` doesn't thrash on every
|
|
47
|
-
// render. scrollContainer is a getter invoked lazily on every event inside
|
|
48
|
-
// the utility — swapping its reference doesn't change the resolved element,
|
|
49
|
-
// so we intentionally omit it from deps to keep inline getters stable.
|
|
50
|
-
const srMode = scrollRestoration?.mode;
|
|
51
|
-
const srAnchor = scrollRestoration?.anchorScrolling;
|
|
52
|
-
const srBehavior = scrollRestoration?.behavior;
|
|
53
|
-
const srStorageKey = scrollRestoration?.storageKey;
|
|
54
|
-
const srEnabled = scrollRestoration !== undefined;
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (!srEnabled) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const sr = createScrollRestoration(router, {
|
|
62
|
-
mode: srMode,
|
|
63
|
-
anchorScrolling: srAnchor,
|
|
64
|
-
behavior: srBehavior,
|
|
65
|
-
storageKey: srStorageKey,
|
|
66
|
-
// srEnabled check above guarantees scrollRestoration is defined.
|
|
67
|
-
scrollContainer: scrollRestoration.scrollContainer,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return () => {
|
|
71
|
-
sr.destroy();
|
|
72
|
-
};
|
|
73
|
-
// scrollRestoration (for scrollContainer) omitted — see comment above.
|
|
74
|
-
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
75
|
-
}, [router, srEnabled, srMode, srAnchor, srBehavior, srStorageKey]);
|
|
76
|
-
|
|
77
|
-
const spySelector = scrollSpy?.selector;
|
|
78
|
-
const spyRootMargin = scrollSpy?.rootMargin;
|
|
79
|
-
const spyEnabled =
|
|
80
|
-
scrollSpy !== undefined && spySelector !== undefined && spySelector !== "";
|
|
81
|
-
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!spyEnabled) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const spy = createScrollSpy(router, {
|
|
88
|
-
selector: spySelector,
|
|
89
|
-
rootMargin: spyRootMargin,
|
|
90
|
-
scrollContainer: scrollSpy.scrollContainer,
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
spy.destroy();
|
|
95
|
-
};
|
|
96
|
-
// scrollSpy (for scrollContainer) omitted — same rationale as
|
|
97
|
-
// scrollRestoration above: getter is invoked lazily inside the utility,
|
|
98
|
-
// identity changes don't affect resolution.
|
|
99
|
-
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
100
|
-
}, [router, spyEnabled, spySelector, spyRootMargin]);
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
if (!viewTransitions) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const vt = createViewTransitions(router);
|
|
108
|
-
|
|
109
|
-
return () => {
|
|
110
|
-
vt.destroy();
|
|
111
|
-
};
|
|
112
|
-
}, [router, viewTransitions]);
|
|
113
|
-
|
|
114
|
-
const navigator = useMemo(() => getNavigator(router), [router]);
|
|
115
|
-
|
|
116
|
-
// useSyncExternalStore manages the router subscription lifecycle:
|
|
117
|
-
// subscribe connects to router on first listener, unsubscribes on last.
|
|
118
|
-
// This is Strict Mode safe — no useEffect cleanup needed.
|
|
119
|
-
const store = useMemo(() => createRouteSource(router), [router]);
|
|
120
|
-
// Use snapshot reference directly. createRouteSource via stabilizeState
|
|
121
|
-
// returns the SAME snapshot reference when route.path is unchanged, so
|
|
122
|
-
// useMemo below sees stable deps for idempotent navigations and
|
|
123
|
-
// RouteContext consumers do not re-render.
|
|
124
|
-
const snapshot = useSyncExternalStore(
|
|
125
|
-
store.subscribe,
|
|
126
|
-
store.getSnapshot,
|
|
127
|
-
store.getSnapshot, // SSR: router returns same state on server and client
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const routeContextValue = useMemo(
|
|
131
|
-
() => ({
|
|
132
|
-
navigator,
|
|
133
|
-
route: snapshot.route,
|
|
134
|
-
previousRoute: snapshot.previousRoute,
|
|
135
|
-
}),
|
|
136
|
-
[navigator, snapshot],
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<RouterContext.Provider value={router}>
|
|
141
|
-
<NavigatorContext.Provider value={navigator}>
|
|
142
|
-
<RouteContext.Provider value={routeContextValue}>
|
|
143
|
-
{children}
|
|
144
|
-
</RouteContext.Provider>
|
|
145
|
-
</NavigatorContext.Provider>
|
|
146
|
-
</RouterContext.Provider>
|
|
147
|
-
);
|
|
148
|
-
};
|
package/src/components/Await.tsx
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { use } from "react";
|
|
2
|
-
|
|
3
|
-
import { useDeferred } from "../hooks/useDeferred";
|
|
4
|
-
|
|
5
|
-
import type { ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
export interface AwaitProps<T> {
|
|
8
|
-
/** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
|
|
9
|
-
readonly name: string;
|
|
10
|
-
/** Render the resolved value. Suspends while pending; throws inside the
|
|
11
|
-
* nearest Error Boundary on rejection. */
|
|
12
|
-
readonly children: (value: T) => ReactNode;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Ergonomic wrapper around `useDeferred(name)` + React 19's `use(promise)`.
|
|
17
|
-
*
|
|
18
|
-
* ```tsx
|
|
19
|
-
* <Suspense fallback={<Spinner />}>
|
|
20
|
-
* <Await<Review[]> name="reviews">
|
|
21
|
-
* {(reviews) => <ReviewList items={reviews} />}
|
|
22
|
-
* </Await>
|
|
23
|
-
* </Suspense>
|
|
24
|
-
* ```
|
|
25
|
-
*
|
|
26
|
-
* Equivalent to:
|
|
27
|
-
*
|
|
28
|
-
* ```tsx
|
|
29
|
-
* function Inner() {
|
|
30
|
-
* const reviews = use(useDeferred<Review[]>("reviews"));
|
|
31
|
-
* return <ReviewList items={reviews} />;
|
|
32
|
-
* }
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* Pick `<Await>` for cross-adapter consistency with the SvelteKit
|
|
36
|
-
* `{#await}` / Solid `<Await/>` shape; pick the inline `use(useDeferred(...))`
|
|
37
|
-
* form if you prefer one fewer abstraction.
|
|
38
|
-
*/
|
|
39
|
-
export function Await<T = unknown>({
|
|
40
|
-
name,
|
|
41
|
-
children,
|
|
42
|
-
}: AwaitProps<T>): ReactNode {
|
|
43
|
-
const value = use(useDeferred<T>(name));
|
|
44
|
-
|
|
45
|
-
return children(value);
|
|
46
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
|
|
5
|
-
export interface ClientOnlyProps {
|
|
6
|
-
readonly children: ReactNode;
|
|
7
|
-
readonly fallback?: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ClientOnly({
|
|
11
|
-
children,
|
|
12
|
-
fallback = null,
|
|
13
|
-
}: ClientOnlyProps): ReactNode {
|
|
14
|
-
const [mounted, setMounted] = useState(false);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
// SSR/hydration boundary: server emits the fallback branch, client matches
|
|
18
|
-
// it on first paint, then this effect flips state to swap in the children.
|
|
19
|
-
// The intentional re-render is what makes the markup match across renders.
|
|
20
|
-
// eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap
|
|
21
|
-
setMounted(true);
|
|
22
|
-
}, []);
|
|
23
|
-
|
|
24
|
-
return mounted ? children : fallback;
|
|
25
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { useContext } from "react";
|
|
2
|
-
|
|
3
|
-
import { HttpStatusContext } from "./HttpStatusProvider";
|
|
4
|
-
|
|
5
|
-
import type { ReactNode } from "react";
|
|
6
|
-
|
|
7
|
-
export interface HttpStatusCodeProps {
|
|
8
|
-
/** HTTP status to apply to the response. Common values: 404, 410, 451, 503. */
|
|
9
|
-
readonly code: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Render-time HTTP status declaration. Mount inside a route component (typical
|
|
14
|
-
* use case: a glob `*` route's NotFound page) when the status is decided by
|
|
15
|
-
* the rendered tree rather than a loader.
|
|
16
|
-
*
|
|
17
|
-
* Writes `code` to the nearest `<HttpStatusProvider>`'s sink during render and
|
|
18
|
-
* returns `null`. With no provider mounted (the standard client-side case)
|
|
19
|
-
* the component is a silent no-op — same component tree hydrates without
|
|
20
|
-
* touching the DOM or warning about mismatches.
|
|
21
|
-
*
|
|
22
|
-
* Loader-driven errors (`LoaderNotFound` → 404, `LoaderRedirect` → 30x) keep
|
|
23
|
-
* working as before; this component covers render-time decisions only.
|
|
24
|
-
*
|
|
25
|
-
* Last write wins when several `<HttpStatusCode />` instances mount in the
|
|
26
|
-
* same render pass — sink reflects the last component that ran.
|
|
27
|
-
*
|
|
28
|
-
* ```tsx
|
|
29
|
-
* // entry-server.tsx
|
|
30
|
-
* import { renderToString } from "react-dom/server";
|
|
31
|
-
* import { createHttpStatusSink, HttpStatusProvider } from "@real-router/react/ssr";
|
|
32
|
-
*
|
|
33
|
-
* const sink = createHttpStatusSink();
|
|
34
|
-
* const html = renderToString(
|
|
35
|
-
* <HttpStatusProvider sink={sink}>
|
|
36
|
-
* <RouterProvider router={router}>
|
|
37
|
-
* <App />
|
|
38
|
-
* </RouterProvider>
|
|
39
|
-
* </HttpStatusProvider>,
|
|
40
|
-
* );
|
|
41
|
-
* response.status(sink.code ?? 200).send(html);
|
|
42
|
-
* ```
|
|
43
|
-
*
|
|
44
|
-
* **Streaming SSR (`renderToReadableStream`):** the response status MUST be
|
|
45
|
-
* sent before the first body byte flushes. If `<HttpStatusCode />` is mounted
|
|
46
|
-
* inside a late-resolving `<Suspense>` boundary, the sink write may happen
|
|
47
|
-
* AFTER the headers are already on the wire — the override is then lost.
|
|
48
|
-
* Either mount the component in the shell (above every `<Suspense>` that
|
|
49
|
-
* could delay it) or `await stream.allReady` before reading `sink.code`
|
|
50
|
-
* (which forfeits streaming benefits). For non-streaming SSR
|
|
51
|
-
* (`renderToString`) there is no such ordering concern.
|
|
52
|
-
*
|
|
53
|
-
* **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
|
|
54
|
-
* `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
|
|
55
|
-
* dropped connection, not silent corruption. Pass a real HTTP status integer
|
|
56
|
-
* (commonly 4xx/5xx; 100-999 is what Node accepts).
|
|
57
|
-
*/
|
|
58
|
-
export function HttpStatusCode({ code }: HttpStatusCodeProps): ReactNode {
|
|
59
|
-
const sink = useContext(HttpStatusContext);
|
|
60
|
-
|
|
61
|
-
if (sink) {
|
|
62
|
-
sink.code = code;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { createContext } from "react";
|
|
2
|
-
|
|
3
|
-
import type { HttpStatusSink } from "../utils/createHttpStatusSink";
|
|
4
|
-
import type { ReactNode } from "react";
|
|
5
|
-
|
|
6
|
-
export const HttpStatusContext = createContext<HttpStatusSink | null>(null);
|
|
7
|
-
|
|
8
|
-
export interface HttpStatusProviderProps {
|
|
9
|
-
readonly sink: HttpStatusSink;
|
|
10
|
-
readonly children: ReactNode;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function HttpStatusProvider({
|
|
14
|
-
sink,
|
|
15
|
-
children,
|
|
16
|
-
}: HttpStatusProviderProps): ReactNode {
|
|
17
|
-
// `<HttpStatusContext.Provider value>` (not the React 19 `<HttpStatusContext value>`
|
|
18
|
-
// shorthand) — same component file is exported via `/legacy/ssr` for React 18
|
|
19
|
-
// consumers, where the shorthand throws "Element type is invalid: expected
|
|
20
|
-
// a string but got: object".
|
|
21
|
-
return (
|
|
22
|
-
<HttpStatusContext.Provider value={sink}>
|
|
23
|
-
{children}
|
|
24
|
-
</HttpStatusContext.Provider>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { Text, useFocus, useInput } from "ink";
|
|
2
|
-
import { memo } from "react";
|
|
3
|
-
|
|
4
|
-
import { EMPTY_OPTIONS, EMPTY_PARAMS } from "../constants";
|
|
5
|
-
import { shallowEqual } from "../dom-utils";
|
|
6
|
-
import { useIsActiveRoute } from "../hooks/useIsActiveRoute";
|
|
7
|
-
import { useRouter } from "../hooks/useRouter";
|
|
8
|
-
|
|
9
|
-
import type { InkLinkProps } from "../ink-types";
|
|
10
|
-
import type { FC } from "react";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Pick the most specific value across focus / active / base priority.
|
|
14
|
-
* Focus wins over active; active wins over base. Each layer falls through
|
|
15
|
-
* to the next when its value is undefined, matching the original explicit
|
|
16
|
-
* if/else cascade for both `color` and `inverse`.
|
|
17
|
-
*/
|
|
18
|
-
function pickPriority<T>(
|
|
19
|
-
isFocused: boolean,
|
|
20
|
-
isRouteActive: boolean,
|
|
21
|
-
focusValue: T | undefined,
|
|
22
|
-
activeValue: T | undefined,
|
|
23
|
-
baseValue: T | undefined,
|
|
24
|
-
): T | undefined {
|
|
25
|
-
if (isFocused) {
|
|
26
|
-
return focusValue ?? activeValue ?? baseValue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (isRouteActive) {
|
|
30
|
-
return activeValue ?? baseValue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return baseValue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function areInkLinkPropsEqual(
|
|
37
|
-
prev: Readonly<InkLinkProps>,
|
|
38
|
-
next: Readonly<InkLinkProps>,
|
|
39
|
-
): boolean {
|
|
40
|
-
return (
|
|
41
|
-
prev.routeName === next.routeName &&
|
|
42
|
-
prev.activeStrict === next.activeStrict &&
|
|
43
|
-
prev.ignoreQueryParams === next.ignoreQueryParams &&
|
|
44
|
-
prev.color === next.color &&
|
|
45
|
-
prev.activeColor === next.activeColor &&
|
|
46
|
-
prev.focusColor === next.focusColor &&
|
|
47
|
-
prev.inverse === next.inverse &&
|
|
48
|
-
prev.activeInverse === next.activeInverse &&
|
|
49
|
-
prev.focusInverse === next.focusInverse &&
|
|
50
|
-
prev.id === next.id &&
|
|
51
|
-
prev.autoFocus === next.autoFocus &&
|
|
52
|
-
prev.onSelect === next.onSelect &&
|
|
53
|
-
prev.children === next.children &&
|
|
54
|
-
shallowEqual(prev.routeParams, next.routeParams) &&
|
|
55
|
-
shallowEqual(prev.routeOptions, next.routeOptions)
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const InkLinkImpl: FC<InkLinkProps> = ({
|
|
60
|
-
routeName,
|
|
61
|
-
routeParams = EMPTY_PARAMS,
|
|
62
|
-
routeOptions = EMPTY_OPTIONS,
|
|
63
|
-
activeStrict = false,
|
|
64
|
-
ignoreQueryParams = true,
|
|
65
|
-
color,
|
|
66
|
-
activeColor,
|
|
67
|
-
focusColor,
|
|
68
|
-
inverse,
|
|
69
|
-
activeInverse,
|
|
70
|
-
focusInverse,
|
|
71
|
-
id,
|
|
72
|
-
autoFocus,
|
|
73
|
-
onSelect,
|
|
74
|
-
children,
|
|
75
|
-
}) => {
|
|
76
|
-
const router = useRouter();
|
|
77
|
-
const { isFocused } = useFocus({
|
|
78
|
-
...(id !== undefined && { id }),
|
|
79
|
-
...(autoFocus !== undefined && { autoFocus }),
|
|
80
|
-
});
|
|
81
|
-
const isRouteActive = useIsActiveRoute(
|
|
82
|
-
routeName,
|
|
83
|
-
routeParams,
|
|
84
|
-
activeStrict,
|
|
85
|
-
ignoreQueryParams,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// No useCallback: `useInput` consumes the handler via its own internal
|
|
89
|
-
// ref; a stable identity provides no cache-bail-out benefit here. Same
|
|
90
|
-
// reasoning as Link's handleClick.
|
|
91
|
-
useInput(
|
|
92
|
-
(_input, key) => {
|
|
93
|
-
if (key.return) {
|
|
94
|
-
onSelect?.();
|
|
95
|
-
router.navigate(routeName, routeParams, routeOptions).catch(() => {});
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
{ isActive: isFocused },
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const finalColor = pickPriority(
|
|
102
|
-
isFocused,
|
|
103
|
-
isRouteActive,
|
|
104
|
-
focusColor,
|
|
105
|
-
activeColor,
|
|
106
|
-
color,
|
|
107
|
-
);
|
|
108
|
-
const finalInverse = pickPriority(
|
|
109
|
-
isFocused,
|
|
110
|
-
isRouteActive,
|
|
111
|
-
focusInverse,
|
|
112
|
-
activeInverse,
|
|
113
|
-
inverse,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
const textProps: { color?: string; inverse?: boolean } = {};
|
|
117
|
-
|
|
118
|
-
if (finalColor !== undefined) {
|
|
119
|
-
textProps.color = finalColor;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (finalInverse !== undefined) {
|
|
123
|
-
textProps.inverse = finalInverse;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return <Text {...textProps}>{children}</Text>;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
export const InkLink: FC<InkLinkProps> = memo(
|
|
130
|
-
InkLinkImpl,
|
|
131
|
-
areInkLinkPropsEqual,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
InkLink.displayName = "InkLink";
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { RouterProvider } from "../RouterProvider";
|
|
2
|
-
|
|
3
|
-
import type { InkRouterProviderProps } from "../ink-types";
|
|
4
|
-
import type { FC } from "react";
|
|
5
|
-
|
|
6
|
-
export const InkRouterProvider: FC<InkRouterProviderProps> = ({
|
|
7
|
-
router,
|
|
8
|
-
children,
|
|
9
|
-
}) => <RouterProvider router={router}>{children}</RouterProvider>;
|
package/src/components/Link.tsx
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { memo, useMemo } from "react";
|
|
2
|
-
|
|
3
|
-
import { EMPTY_PARAMS, EMPTY_OPTIONS } from "../constants";
|
|
4
|
-
import {
|
|
5
|
-
shouldNavigate,
|
|
6
|
-
buildHref,
|
|
7
|
-
buildActiveClassName,
|
|
8
|
-
navigateWithHash,
|
|
9
|
-
shallowEqual,
|
|
10
|
-
} from "../dom-utils";
|
|
11
|
-
import { useIsActiveRoute } from "../hooks/useIsActiveRoute";
|
|
12
|
-
import { useRouter } from "../hooks/useRouter";
|
|
13
|
-
|
|
14
|
-
import type { LinkProps } from "../types";
|
|
15
|
-
import type { FC, MouseEvent } from "react";
|
|
16
|
-
|
|
17
|
-
function areLinkPropsEqual(
|
|
18
|
-
prev: Readonly<LinkProps>,
|
|
19
|
-
next: Readonly<LinkProps>,
|
|
20
|
-
): boolean {
|
|
21
|
-
return (
|
|
22
|
-
prev.routeName === next.routeName &&
|
|
23
|
-
prev.className === next.className &&
|
|
24
|
-
prev.activeClassName === next.activeClassName &&
|
|
25
|
-
prev.activeStrict === next.activeStrict &&
|
|
26
|
-
prev.ignoreQueryParams === next.ignoreQueryParams &&
|
|
27
|
-
prev.onClick === next.onClick &&
|
|
28
|
-
prev.target === next.target &&
|
|
29
|
-
prev.style === next.style &&
|
|
30
|
-
prev.children === next.children &&
|
|
31
|
-
prev.hash === next.hash &&
|
|
32
|
-
shallowEqual(prev.routeParams, next.routeParams) &&
|
|
33
|
-
shallowEqual(prev.routeOptions, next.routeOptions)
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const LinkImpl: FC<LinkProps> = ({
|
|
38
|
-
routeName,
|
|
39
|
-
routeParams = EMPTY_PARAMS,
|
|
40
|
-
routeOptions = EMPTY_OPTIONS,
|
|
41
|
-
className,
|
|
42
|
-
activeClassName = "active",
|
|
43
|
-
activeStrict = false,
|
|
44
|
-
ignoreQueryParams = true,
|
|
45
|
-
hash,
|
|
46
|
-
onClick,
|
|
47
|
-
target,
|
|
48
|
-
children,
|
|
49
|
-
...props
|
|
50
|
-
}) => {
|
|
51
|
-
// memo + areLinkPropsEqual guarantees that on bail-out the component does
|
|
52
|
-
// not render; on render, routeParams/routeOptions either changed reference
|
|
53
|
-
// (true change) or comparator failed (e.g., BigInt fallback to identity),
|
|
54
|
-
// so they're safe to use directly in hook deps.
|
|
55
|
-
|
|
56
|
-
const router = useRouter();
|
|
57
|
-
|
|
58
|
-
// When `hash` prop is set, active state requires both route AND hash to
|
|
59
|
-
// match (#532). Without this, three tab links sharing routeName="settings"
|
|
60
|
-
// would all be marked active by route-name alone, defeating tab semantics.
|
|
61
|
-
const isActive = useIsActiveRoute(
|
|
62
|
-
routeName,
|
|
63
|
-
routeParams,
|
|
64
|
-
activeStrict,
|
|
65
|
-
ignoreQueryParams,
|
|
66
|
-
hash,
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
// No useMemo: outer memo()+shallowEqual prevents Link re-render unless a
|
|
70
|
-
// prop actually changed. When this body runs, either `hash` differs from
|
|
71
|
-
// last render or another prop changed — in both cases the `{ hash }` alloc
|
|
72
|
-
// is unavoidable. The useMemo wrapper added one closure + deps slot per
|
|
73
|
-
// render without saving an allocation.
|
|
74
|
-
const hashOption = hash === undefined ? undefined : { hash };
|
|
75
|
-
const href = buildHref(router, routeName, routeParams, hashOption);
|
|
76
|
-
|
|
77
|
-
// useCallback was wasteful: 7 deps recreated the closure on every meaningful
|
|
78
|
-
// render anyway, and `<a onClick>` does not benefit from a stable function
|
|
79
|
-
// identity (no child-memo-bail-out chain past it). Inline arrow function is
|
|
80
|
-
// what React Compiler emits automatically for this shape.
|
|
81
|
-
const handleClick = (evt: MouseEvent<HTMLAnchorElement>) => {
|
|
82
|
-
if (onClick) {
|
|
83
|
-
onClick(evt);
|
|
84
|
-
|
|
85
|
-
if (evt.defaultPrevented) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!shouldNavigate(evt.nativeEvent) || target === "_blank") {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
evt.preventDefault();
|
|
95
|
-
navigateWithHash(router, routeName, routeParams, hash, routeOptions).catch(
|
|
96
|
-
() => {},
|
|
97
|
-
);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Memoize the joined class string. parseTokens + Set + join on every render
|
|
101
|
-
// adds up on pages with N Links navigating frequently; deps cover every
|
|
102
|
-
// input the function reads so cache invalidation is exact.
|
|
103
|
-
const finalClassName = useMemo(
|
|
104
|
-
() => buildActiveClassName(isActive, activeClassName, className),
|
|
105
|
-
[isActive, activeClassName, className],
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<a {...props} href={href} className={finalClassName} onClick={handleClick}>
|
|
110
|
-
{children}
|
|
111
|
-
</a>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export const Link: FC<LinkProps> = memo(LinkImpl, areLinkPropsEqual);
|
|
116
|
-
|
|
117
|
-
Link.displayName = "Link";
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { createDismissableError } from "@real-router/sources";
|
|
2
|
-
import {
|
|
3
|
-
useEffect,
|
|
4
|
-
useLayoutEffect,
|
|
5
|
-
useMemo,
|
|
6
|
-
useRef,
|
|
7
|
-
useSyncExternalStore,
|
|
8
|
-
} from "react";
|
|
9
|
-
|
|
10
|
-
import { useRouter } from "../hooks/useRouter";
|
|
11
|
-
|
|
12
|
-
import type { RouterError, State } from "@real-router/core";
|
|
13
|
-
import type { ReactElement, ReactNode } from "react";
|
|
14
|
-
|
|
15
|
-
export interface RouterErrorBoundaryProps {
|
|
16
|
-
readonly children: ReactNode;
|
|
17
|
-
readonly fallback: (error: RouterError, resetError: () => void) => ReactNode;
|
|
18
|
-
readonly onError?: (
|
|
19
|
-
error: RouterError,
|
|
20
|
-
toRoute: State | null,
|
|
21
|
-
fromRoute: State | null,
|
|
22
|
-
) => void;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function RouterErrorBoundary({
|
|
26
|
-
children,
|
|
27
|
-
fallback,
|
|
28
|
-
onError,
|
|
29
|
-
}: RouterErrorBoundaryProps): ReactElement {
|
|
30
|
-
const router = useRouter();
|
|
31
|
-
// Per-router cached in @real-router/sources — the WeakMap lookup is cheap,
|
|
32
|
-
// but useMemo avoids it entirely on stable-router re-renders (the common
|
|
33
|
-
// case for boundaries mounted in app shells).
|
|
34
|
-
const store = useMemo(() => createDismissableError(router), [router]);
|
|
35
|
-
const snapshot = useSyncExternalStore(
|
|
36
|
-
store.subscribe,
|
|
37
|
-
store.getSnapshot,
|
|
38
|
-
store.getSnapshot,
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const onErrorRef = useRef(onError);
|
|
42
|
-
|
|
43
|
-
// "Latest ref" pattern shared with useRouteEnter/useRouteExit: sync the
|
|
44
|
-
// callback to the ref via useLayoutEffect (synchronous, post-render,
|
|
45
|
-
// pre-paint) so the snapshot effect below reads the freshest callback
|
|
46
|
-
// without listing `onError` as a dep — and StrictMode's double-effect
|
|
47
|
-
// pass writes the same value twice harmlessly.
|
|
48
|
-
useLayoutEffect(() => {
|
|
49
|
-
onErrorRef.current = onError;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (snapshot.error) {
|
|
54
|
-
onErrorRef.current?.(
|
|
55
|
-
snapshot.error,
|
|
56
|
-
snapshot.toRoute,
|
|
57
|
-
snapshot.fromRoute,
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
// eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback
|
|
61
|
-
}, [snapshot.version]);
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<>
|
|
65
|
-
{children}
|
|
66
|
-
{snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}
|
|
67
|
-
</>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
2
|
-
|
|
3
|
-
import type { ReactNode } from "react";
|
|
4
|
-
|
|
5
|
-
export interface ServerOnlyProps {
|
|
6
|
-
readonly children: ReactNode;
|
|
7
|
-
readonly fallback?: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ServerOnly({
|
|
11
|
-
children,
|
|
12
|
-
fallback = null,
|
|
13
|
-
}: ServerOnlyProps): ReactNode {
|
|
14
|
-
const [mounted, setMounted] = useState(false);
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
// SSR/hydration boundary: server emits the children branch, client matches
|
|
18
|
-
// it on first paint, then this effect flips state to swap in the fallback
|
|
19
|
-
// (or hide entirely). The intentional re-render keeps markup consistent
|
|
20
|
-
// across renders.
|
|
21
|
-
// eslint-disable-next-line @eslint-react/set-state-in-effect -- intentional post-hydration swap
|
|
22
|
-
setMounted(true);
|
|
23
|
-
}, []);
|
|
24
|
-
|
|
25
|
-
return mounted ? fallback : children;
|
|
26
|
-
}
|