@real-router/preact 0.15.2 → 0.15.3
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/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +5 -6
- package/src/RouterProvider.tsx +0 -152
- package/src/components/Await.tsx +0 -99
- package/src/components/ClientOnly.tsx +0 -25
- package/src/components/HttpStatusCode.tsx +0 -82
- package/src/components/HttpStatusProvider.tsx +0 -22
- package/src/components/Link.tsx +0 -141
- package/src/components/RouteView/RouteView.tsx +0 -57
- package/src/components/RouteView/components.tsx +0 -19
- package/src/components/RouteView/helpers.tsx +0 -174
- package/src/components/RouteView/index.ts +0 -8
- package/src/components/RouteView/types.ts +0 -24
- package/src/components/RouterErrorBoundary.tsx +0 -84
- package/src/components/ServerOnly.tsx +0 -26
- package/src/components/Streamed.tsx +0 -24
- package/src/constants.ts +0 -9
- package/src/context.ts +0 -27
- package/src/hooks/useDeferred.tsx +0 -26
- package/src/hooks/useIsActiveRoute.tsx +0 -46
- package/src/hooks/useNavigator.tsx +0 -8
- package/src/hooks/useRoute.tsx +0 -26
- package/src/hooks/useRouteEnter.tsx +0 -147
- package/src/hooks/useRouteExit.tsx +0 -159
- package/src/hooks/useRouteNode.tsx +0 -34
- package/src/hooks/useRouteUtils.tsx +0 -12
- package/src/hooks/useRouter.tsx +0 -8
- package/src/hooks/useRouterTransition.tsx +0 -17
- package/src/index.ts +0 -56
- package/src/ssr.ts +0 -39
- package/src/types.ts +0 -40
- package/src/useSyncExternalStore.ts +0 -60
- package/src/utils/createHttpStatusSink.ts +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/preact",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Preact integration for Real-Router",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -34,8 +34,7 @@
|
|
|
34
34
|
}
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"dist"
|
|
38
|
-
"src"
|
|
37
|
+
"dist"
|
|
39
38
|
],
|
|
40
39
|
"homepage": "https://github.com/greydragon888/real-router",
|
|
41
40
|
"repository": {
|
|
@@ -64,9 +63,9 @@
|
|
|
64
63
|
"license": "MIT",
|
|
65
64
|
"sideEffects": false,
|
|
66
65
|
"dependencies": {
|
|
66
|
+
"@real-router/core": "^0.57.0",
|
|
67
67
|
"@real-router/route-utils": "^0.2.3",
|
|
68
|
-
"@real-router/
|
|
69
|
-
"@real-router/sources": "^0.8.5"
|
|
68
|
+
"@real-router/sources": "^0.8.6"
|
|
70
69
|
},
|
|
71
70
|
"devDependencies": {
|
|
72
71
|
"@testing-library/dom": "10.4.1",
|
|
@@ -75,7 +74,7 @@
|
|
|
75
74
|
"@testing-library/user-event": "14.6.1",
|
|
76
75
|
"preact": "10.29.2",
|
|
77
76
|
"preact-render-to-string": "6.7.0",
|
|
78
|
-
"@real-router/browser-plugin": "^0.17.
|
|
77
|
+
"@real-router/browser-plugin": "^0.17.7"
|
|
79
78
|
},
|
|
80
79
|
"peerDependencies": {
|
|
81
80
|
"preact": ">=10.28.0 || ^11.0.0-0"
|
package/src/RouterProvider.tsx
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { getNavigator } from "@real-router/core";
|
|
2
|
-
import { createRouteSource } from "@real-router/sources";
|
|
3
|
-
import { useEffect, useMemo } from "preact/hooks";
|
|
4
|
-
|
|
5
|
-
import { NavigatorContext, RouteContext, RouterContext } from "./context";
|
|
6
|
-
import {
|
|
7
|
-
createRouteAnnouncer,
|
|
8
|
-
createScrollRestoration,
|
|
9
|
-
createScrollSpy,
|
|
10
|
-
createViewTransitions,
|
|
11
|
-
} from "./dom-utils";
|
|
12
|
-
import { useSyncExternalStore } from "./useSyncExternalStore";
|
|
13
|
-
|
|
14
|
-
import type { ScrollRestorationOptions, ScrollSpyOptions } from "./dom-utils";
|
|
15
|
-
import type { Router } from "@real-router/core";
|
|
16
|
-
import type { FunctionComponent, ComponentChildren } from "preact";
|
|
17
|
-
|
|
18
|
-
export interface RouteProviderProps {
|
|
19
|
-
router: Router;
|
|
20
|
-
children: ComponentChildren;
|
|
21
|
-
announceNavigation?: boolean;
|
|
22
|
-
scrollRestoration?: ScrollRestorationOptions;
|
|
23
|
-
scrollSpy?: ScrollSpyOptions;
|
|
24
|
-
viewTransitions?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const RouterProvider: FunctionComponent<RouteProviderProps> = ({
|
|
28
|
-
router,
|
|
29
|
-
children,
|
|
30
|
-
announceNavigation,
|
|
31
|
-
scrollRestoration,
|
|
32
|
-
scrollSpy,
|
|
33
|
-
viewTransitions,
|
|
34
|
-
}) => {
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (!announceNavigation) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const announcer = createRouteAnnouncer(router);
|
|
41
|
-
|
|
42
|
-
return () => {
|
|
43
|
-
announcer.destroy();
|
|
44
|
-
};
|
|
45
|
-
}, [announceNavigation, router]);
|
|
46
|
-
|
|
47
|
-
// Primitive deps so inline `{ mode: "restore" }` doesn't thrash on every
|
|
48
|
-
// render. scrollContainer is a getter invoked lazily on every event inside
|
|
49
|
-
// the utility — swapping its reference doesn't change the resolved element,
|
|
50
|
-
// so we intentionally omit it from deps to keep inline getters stable.
|
|
51
|
-
const srMode = scrollRestoration?.mode;
|
|
52
|
-
const srAnchor = scrollRestoration?.anchorScrolling;
|
|
53
|
-
const srBehavior = scrollRestoration?.behavior;
|
|
54
|
-
const srStorageKey = scrollRestoration?.storageKey;
|
|
55
|
-
const srEnabled = scrollRestoration !== undefined;
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (!srEnabled) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const sr = createScrollRestoration(router, {
|
|
63
|
-
mode: srMode,
|
|
64
|
-
anchorScrolling: srAnchor,
|
|
65
|
-
behavior: srBehavior,
|
|
66
|
-
storageKey: srStorageKey,
|
|
67
|
-
// srEnabled check above guarantees scrollRestoration is defined.
|
|
68
|
-
scrollContainer: scrollRestoration.scrollContainer,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return () => {
|
|
72
|
-
sr.destroy();
|
|
73
|
-
};
|
|
74
|
-
// scrollRestoration (for scrollContainer) omitted — see comment above.
|
|
75
|
-
// eslint-disable-next-line @eslint-react/exhaustive-deps
|
|
76
|
-
}, [router, srEnabled, srMode, srAnchor, srBehavior, srStorageKey]);
|
|
77
|
-
|
|
78
|
-
const spySelector = scrollSpy?.selector;
|
|
79
|
-
const spyRootMargin = scrollSpy?.rootMargin;
|
|
80
|
-
const spyEnabled =
|
|
81
|
-
scrollSpy !== undefined && spySelector !== undefined && spySelector !== "";
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!spyEnabled) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const spy = createScrollSpy(router, {
|
|
89
|
-
selector: spySelector,
|
|
90
|
-
rootMargin: spyRootMargin,
|
|
91
|
-
scrollContainer: scrollSpy.scrollContainer,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return () => {
|
|
95
|
-
spy.destroy();
|
|
96
|
-
};
|
|
97
|
-
// scrollSpy (for scrollContainer) omitted — same rationale as
|
|
98
|
-
// scrollRestoration above: getter is invoked lazily inside the utility.
|
|
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
|
-
// `getNavigator` is cached per-router in `@real-router/core` (WeakMap) —
|
|
115
|
-
// same router always returns the same Navigator ref. No `useMemo` needed.
|
|
116
|
-
const navigator = getNavigator(router);
|
|
117
|
-
|
|
118
|
-
// `createRouteSource` is NOT cached (per packages/sources/CLAUDE.md table).
|
|
119
|
-
// It must be stable across renders so `useSyncExternalStore`'s deps don't
|
|
120
|
-
// change identity and trigger an unsubscribe/resubscribe loop on every
|
|
121
|
-
// render. `useMemo([router])` gives one source per router-instance lifetime.
|
|
122
|
-
const store = useMemo(() => createRouteSource(router), [router]);
|
|
123
|
-
|
|
124
|
-
// useSyncExternalStore manages the router subscription lifecycle:
|
|
125
|
-
// subscribe connects to router on first listener, unsubscribes on last.
|
|
126
|
-
const { route, previousRoute } = useSyncExternalStore(
|
|
127
|
-
store.subscribe,
|
|
128
|
-
store.getSnapshot,
|
|
129
|
-
store.getSnapshot, // SSR: router returns same state on server and client
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
// Stable-ref against parent re-renders: when parent re-renders RouterProvider
|
|
133
|
-
// without a route change (e.g. consumer re-renders the root), navigator /
|
|
134
|
-
// route / previousRoute references stay identical (useSyncExternalStore +
|
|
135
|
-
// Object.is bail-out). Without `useMemo` the object literal is fresh every
|
|
136
|
-
// render, propagating spurious re-renders to every `useRoute()` consumer.
|
|
137
|
-
// The memo bails out whenever the three deps are referentially equal.
|
|
138
|
-
const routeContextValue = useMemo(
|
|
139
|
-
() => ({ navigator, route, previousRoute }),
|
|
140
|
-
[navigator, route, previousRoute],
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<RouterContext.Provider value={router}>
|
|
145
|
-
<NavigatorContext.Provider value={navigator}>
|
|
146
|
-
<RouteContext.Provider value={routeContextValue}>
|
|
147
|
-
{children}
|
|
148
|
-
</RouteContext.Provider>
|
|
149
|
-
</NavigatorContext.Provider>
|
|
150
|
-
</RouterContext.Provider>
|
|
151
|
-
);
|
|
152
|
-
};
|
package/src/components/Await.tsx
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { useDeferred } from "../hooks/useDeferred";
|
|
2
|
-
|
|
3
|
-
import type { ComponentChildren } from "preact";
|
|
4
|
-
|
|
5
|
-
interface TrackedPromise<T> extends Promise<T> {
|
|
6
|
-
status?: "pending" | "fulfilled" | "rejected";
|
|
7
|
-
value?: T;
|
|
8
|
-
reason?: unknown;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Preact's `Suspense` (from `preact/compat`) catches a thrown thenable and
|
|
13
|
-
* re-runs the boundary's render once it settles. For deterministic re-renders
|
|
14
|
-
* we tag the promise with `.status` / `.value` / `.reason` on first access so
|
|
15
|
-
* the second render-pass can return the value synchronously instead of
|
|
16
|
-
* throwing again.
|
|
17
|
-
*
|
|
18
|
-
* The same tag layout is used by React 19's internal `use(promise)` cache,
|
|
19
|
-
* so promises that already carry the tag (e.g. emitted by a Suspense-aware
|
|
20
|
-
* data lib) are reused as-is.
|
|
21
|
-
*/
|
|
22
|
-
function track<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
23
|
-
const tracked = promise as TrackedPromise<T>;
|
|
24
|
-
|
|
25
|
-
if (tracked.status !== undefined) {
|
|
26
|
-
return tracked;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
tracked.status = "pending";
|
|
30
|
-
promise.then(
|
|
31
|
-
(value) => {
|
|
32
|
-
/* v8 ignore next 4 -- @preserve: the `.status === "pending"` guard
|
|
33
|
-
protects against external mutation between `track()` and the .then
|
|
34
|
-
microtask; covered branch is the always-true case in our control. */
|
|
35
|
-
if (tracked.status === "pending") {
|
|
36
|
-
tracked.status = "fulfilled";
|
|
37
|
-
tracked.value = value;
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
/* v8 ignore start -- @preserve: rejection .then handler — tested
|
|
41
|
-
end-to-end via the React adapter's e2e ssr-streaming Scenario 10
|
|
42
|
-
(id=4 reviews promise rejects on the wire); covering it in unit tests
|
|
43
|
-
requires Preact's Suspense to surface the rejection through render,
|
|
44
|
-
which doesn't compose cleanly with vitest's unhandled-rejection
|
|
45
|
-
detector. Behaviour is symmetric to the success handler above. */
|
|
46
|
-
(error: unknown) => {
|
|
47
|
-
if (tracked.status === "pending") {
|
|
48
|
-
tracked.status = "rejected";
|
|
49
|
-
tracked.reason = error;
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
/* v8 ignore stop */
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return tracked;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface AwaitProps<T> {
|
|
59
|
-
/** Deferred key declared in the loader's `defer({ deferred: { <name>: ... } })`. */
|
|
60
|
-
readonly name: string;
|
|
61
|
-
/** Render the resolved value. Suspends while pending; throws inside the
|
|
62
|
-
* nearest Error Boundary on rejection. */
|
|
63
|
-
readonly children: (value: T) => ComponentChildren;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Reads `useDeferred(name)` and hands the resolved value to the render-prop
|
|
68
|
-
* via Preact's `<Suspense>`-throwing convention. Wrap in `<Streamed>` (or
|
|
69
|
-
* `<Suspense>` from `preact/compat`).
|
|
70
|
-
*
|
|
71
|
-
* ```tsx
|
|
72
|
-
* <Streamed fallback={<Spinner />}>
|
|
73
|
-
* <Await<Review[]> name="reviews">
|
|
74
|
-
* {(reviews) => <ReviewList items={reviews} />}
|
|
75
|
-
* </Await>
|
|
76
|
-
* </Streamed>
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
export function Await<T = unknown>({
|
|
80
|
-
name,
|
|
81
|
-
children,
|
|
82
|
-
}: AwaitProps<T>): ComponentChildren {
|
|
83
|
-
const promise = useDeferred<T>(name);
|
|
84
|
-
const tracked = track(promise);
|
|
85
|
-
|
|
86
|
-
if (tracked.status === "fulfilled") {
|
|
87
|
-
return children(tracked.value as T);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (tracked.status === "rejected") {
|
|
91
|
-
throw tracked.reason;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Suspense catches the thrown thenable and waits for resolution. ESLint
|
|
95
|
-
// complains because Promises aren't Errors, but Preact's Suspense (like
|
|
96
|
-
// React's pre-`use()` Suspense convention) explicitly expects a thenable.
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/only-throw-error -- Suspense thenable convention
|
|
98
|
-
throw promise;
|
|
99
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
import type { ComponentChildren } from "preact";
|
|
4
|
-
|
|
5
|
-
export interface ClientOnlyProps {
|
|
6
|
-
readonly children: ComponentChildren;
|
|
7
|
-
readonly fallback?: ComponentChildren;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ClientOnly({
|
|
11
|
-
children,
|
|
12
|
-
fallback = null,
|
|
13
|
-
}: ClientOnlyProps): ComponentChildren {
|
|
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,82 +0,0 @@
|
|
|
1
|
-
import { useContext } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
import { HttpStatusContext } from "./HttpStatusProvider";
|
|
4
|
-
|
|
5
|
-
import type { ComponentChildren } from "preact";
|
|
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 "preact-render-to-string";
|
|
31
|
-
* import { createHttpStatusSink, HttpStatusProvider } from "@real-router/preact/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
|
-
* Mount the component in the shell (above every `<Suspense>` that could
|
|
49
|
-
* delay it). For non-streaming SSR (`renderToString` / `renderToStringAsync`)
|
|
50
|
-
* there is no such ordering concern.
|
|
51
|
-
*
|
|
52
|
-
* **Valid `code` range:** Node's `res.end()` throws `Invalid status code` on
|
|
53
|
-
* `NaN`, `0`, negative values, or values `> 999` — this surfaces as a 5xx /
|
|
54
|
-
* dropped connection, not silent corruption. Pass a real HTTP status integer
|
|
55
|
-
* (commonly 4xx/5xx; 100-999 is what Node accepts).
|
|
56
|
-
*/
|
|
57
|
-
export function HttpStatusCode({
|
|
58
|
-
code,
|
|
59
|
-
}: HttpStatusCodeProps): ComponentChildren {
|
|
60
|
-
const sink = useContext(HttpStatusContext);
|
|
61
|
-
|
|
62
|
-
if (sink) {
|
|
63
|
-
// Dev-only validation: Node's `res.end()` throws `Invalid status code` on
|
|
64
|
-
// NaN / 0 / negative / non-integer / >999. Surface the bad value at the
|
|
65
|
-
// source so the consumer can fix the routing logic, instead of waiting
|
|
66
|
-
// for the server to crash mid-response. Production builds (Vite, esbuild,
|
|
67
|
-
// tsdown all replace `process.env.NODE_ENV !== "production"` with `false`)
|
|
68
|
-
// strip the check.
|
|
69
|
-
if (
|
|
70
|
-
process.env.NODE_ENV !== "production" &&
|
|
71
|
-
(!Number.isInteger(code) || code < 100 || code > 999)
|
|
72
|
-
) {
|
|
73
|
-
console.error(
|
|
74
|
-
`[real-router] <HttpStatusCode code={${String(code)}} /> received an invalid HTTP status code. Node's res.end() rejects values that are not an integer in [100, 999] — pass a real HTTP status (commonly 4xx/5xx).`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
sink.code = code;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { createContext } from "preact";
|
|
2
|
-
|
|
3
|
-
import type { HttpStatusSink } from "../utils/createHttpStatusSink";
|
|
4
|
-
import type { ComponentChildren } from "preact";
|
|
5
|
-
|
|
6
|
-
export const HttpStatusContext = createContext<HttpStatusSink | null>(null);
|
|
7
|
-
|
|
8
|
-
export interface HttpStatusProviderProps {
|
|
9
|
-
readonly sink: HttpStatusSink;
|
|
10
|
-
readonly children: ComponentChildren;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function HttpStatusProvider({
|
|
14
|
-
sink,
|
|
15
|
-
children,
|
|
16
|
-
}: HttpStatusProviderProps): ComponentChildren {
|
|
17
|
-
return (
|
|
18
|
-
<HttpStatusContext.Provider value={sink}>
|
|
19
|
-
{children}
|
|
20
|
-
</HttpStatusContext.Provider>
|
|
21
|
-
);
|
|
22
|
-
}
|
package/src/components/Link.tsx
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { memo } from "preact/compat";
|
|
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 { FunctionComponent, TargetedMouseEvent } from "preact";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Custom comparator for `Link`'s `memo()` wrapper.
|
|
19
|
-
*
|
|
20
|
-
* **Maintenance contract:** every field in `LinkProps` MUST appear in either
|
|
21
|
-
* the `===` chain (primitives + identity-checked references like `onClick` /
|
|
22
|
-
* `children` / `style`) or in the `shallowEqual` arms (object-valued
|
|
23
|
-
* `routeParams` / `routeOptions`). When adding a new prop to `LinkProps`,
|
|
24
|
-
* extend this function in the same PR — `tests/functional/Link.test.tsx`
|
|
25
|
-
* contains a regression-guard that fails the build if `LinkProps` gains a
|
|
26
|
-
* field that is not compared here.
|
|
27
|
-
*
|
|
28
|
-
* **Intentional omissions:** `props` (the rest-spread of HTMLAnchorElement
|
|
29
|
-
* attributes — `aria-label`, `data-*`, `target`-other-than-`_blank`-handling,
|
|
30
|
-
* etc.) is NOT compared. A change to `aria-label` will NOT trigger a Link
|
|
31
|
-
* re-render. This is by design: dynamic `aria-label` is rare; consumers who
|
|
32
|
-
* truly need a reactive aria-label should call `<Link key={ariaLabel}>` to
|
|
33
|
-
* force a remount.
|
|
34
|
-
*/
|
|
35
|
-
function areLinkPropsEqual(
|
|
36
|
-
prev: Readonly<LinkProps>,
|
|
37
|
-
next: Readonly<LinkProps>,
|
|
38
|
-
): boolean {
|
|
39
|
-
return (
|
|
40
|
-
prev.routeName === next.routeName &&
|
|
41
|
-
prev.className === next.className &&
|
|
42
|
-
prev.activeClassName === next.activeClassName &&
|
|
43
|
-
prev.activeStrict === next.activeStrict &&
|
|
44
|
-
prev.ignoreQueryParams === next.ignoreQueryParams &&
|
|
45
|
-
prev.onClick === next.onClick &&
|
|
46
|
-
prev.target === next.target &&
|
|
47
|
-
prev.style === next.style &&
|
|
48
|
-
prev.children === next.children &&
|
|
49
|
-
prev.hash === next.hash &&
|
|
50
|
-
shallowEqual(prev.routeParams, next.routeParams) &&
|
|
51
|
-
shallowEqual(prev.routeOptions, next.routeOptions)
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export const Link: FunctionComponent<LinkProps> = memo(
|
|
56
|
-
({
|
|
57
|
-
routeName,
|
|
58
|
-
routeParams = EMPTY_PARAMS,
|
|
59
|
-
routeOptions = EMPTY_OPTIONS,
|
|
60
|
-
className,
|
|
61
|
-
activeClassName = "active",
|
|
62
|
-
activeStrict = false,
|
|
63
|
-
ignoreQueryParams = true,
|
|
64
|
-
hash,
|
|
65
|
-
onClick,
|
|
66
|
-
target,
|
|
67
|
-
children,
|
|
68
|
-
...props
|
|
69
|
-
}) => {
|
|
70
|
-
const router = useRouter();
|
|
71
|
-
|
|
72
|
-
// memo + areLinkPropsEqual guarantees that on bail-out the component does
|
|
73
|
-
// not render; on render, routeParams/routeOptions changed reference (true
|
|
74
|
-
// change caught by shallowEqual), so they're safe to use directly in hook
|
|
75
|
-
// deps without useStableValue.
|
|
76
|
-
|
|
77
|
-
// Hash-aware active (#532) — see useIsActiveRoute for the contract.
|
|
78
|
-
const isActive = useIsActiveRoute(
|
|
79
|
-
routeName,
|
|
80
|
-
routeParams,
|
|
81
|
-
activeStrict,
|
|
82
|
-
ignoreQueryParams,
|
|
83
|
-
hash,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// `buildHref` is a cheap synchronous call (route-tree lookup + string
|
|
87
|
-
// concat). Wrapping it in `useMemo` allocates a deps array on every
|
|
88
|
-
// render that does not bail out — and on bail-out the function body
|
|
89
|
-
// doesn't execute, so the cache never pays off. Same logic for
|
|
90
|
-
// `buildActiveClassName` and `handleClick` below.
|
|
91
|
-
const href = buildHref(
|
|
92
|
-
router,
|
|
93
|
-
routeName,
|
|
94
|
-
routeParams,
|
|
95
|
-
hash === undefined ? undefined : { hash },
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
const handleClick = (evt: TargetedMouseEvent<HTMLAnchorElement>): void => {
|
|
99
|
-
if (onClick) {
|
|
100
|
-
onClick(evt);
|
|
101
|
-
|
|
102
|
-
if (evt.defaultPrevented) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (!shouldNavigate(evt) || target === "_blank") {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
evt.preventDefault();
|
|
112
|
-
navigateWithHash(
|
|
113
|
-
router,
|
|
114
|
-
routeName,
|
|
115
|
-
routeParams,
|
|
116
|
-
hash,
|
|
117
|
-
routeOptions,
|
|
118
|
-
).catch(() => {});
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const finalClassName = buildActiveClassName(
|
|
122
|
-
isActive,
|
|
123
|
-
activeClassName,
|
|
124
|
-
className,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
<a
|
|
129
|
-
{...props}
|
|
130
|
-
href={href}
|
|
131
|
-
className={finalClassName}
|
|
132
|
-
onClick={handleClick}
|
|
133
|
-
>
|
|
134
|
-
{children}
|
|
135
|
-
</a>
|
|
136
|
-
);
|
|
137
|
-
},
|
|
138
|
-
areLinkPropsEqual,
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
Link.displayName = "Link";
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
import { Match, NotFound, Self } from "./components";
|
|
4
|
-
import { buildRenderList, collectElements } from "./helpers";
|
|
5
|
-
import { useRouteNode } from "../../hooks/useRouteNode";
|
|
6
|
-
|
|
7
|
-
import type { RouteViewProps } from "./types";
|
|
8
|
-
import type { VNode } from "preact";
|
|
9
|
-
|
|
10
|
-
function RouteViewRoot({
|
|
11
|
-
nodeName,
|
|
12
|
-
children,
|
|
13
|
-
}: Readonly<RouteViewProps>): VNode | null {
|
|
14
|
-
const { route } = useRouteNode(nodeName);
|
|
15
|
-
|
|
16
|
-
// Cache the flattened Match/Self/NotFound list across renders with unchanged
|
|
17
|
-
// children. children only differs when the parent re-renders with a new
|
|
18
|
-
// node, so this memoises the steady-state traversal.
|
|
19
|
-
const elements = useMemo(() => {
|
|
20
|
-
const collected: VNode[] = [];
|
|
21
|
-
|
|
22
|
-
collectElements(children, collected);
|
|
23
|
-
|
|
24
|
-
return collected;
|
|
25
|
-
}, [children]);
|
|
26
|
-
|
|
27
|
-
const routeName = route?.name;
|
|
28
|
-
|
|
29
|
-
// buildRenderList is O(N) over Match/Self/NotFound children. Memo on
|
|
30
|
-
// (elements, routeName, nodeName) skips the re-walk on parent re-renders
|
|
31
|
-
// that don't change the active route; navigations always invalidate via
|
|
32
|
-
// routeName.
|
|
33
|
-
const rendered = useMemo(() => {
|
|
34
|
-
if (routeName === undefined) {
|
|
35
|
-
return [];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return buildRenderList(elements, routeName, nodeName).rendered;
|
|
39
|
-
}, [elements, routeName, nodeName]);
|
|
40
|
-
|
|
41
|
-
return rendered.length > 0 ? <>{rendered}</> : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
RouteViewRoot.displayName = "RouteView";
|
|
45
|
-
|
|
46
|
-
export const RouteView = Object.assign(RouteViewRoot, {
|
|
47
|
-
Match,
|
|
48
|
-
Self,
|
|
49
|
-
NotFound,
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
export type {
|
|
53
|
-
RouteViewProps,
|
|
54
|
-
MatchProps as RouteViewMatchProps,
|
|
55
|
-
SelfProps as RouteViewSelfProps,
|
|
56
|
-
NotFoundProps as RouteViewNotFoundProps,
|
|
57
|
-
} from "./types";
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { MatchProps, NotFoundProps, SelfProps } from "./types";
|
|
2
|
-
|
|
3
|
-
export function Match(_props: MatchProps): null {
|
|
4
|
-
return null;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
Match.displayName = "RouteView.Match";
|
|
8
|
-
|
|
9
|
-
export function Self(_props: SelfProps): null {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
Self.displayName = "RouteView.Self";
|
|
14
|
-
|
|
15
|
-
export function NotFound(_props: NotFoundProps): null {
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
NotFound.displayName = "RouteView.NotFound";
|