@real-router/preact 0.15.2 → 0.15.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.
Files changed (40) hide show
  1. package/dist/cjs/index.d.ts +2 -2
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/esm/index.d.mts +2 -2
  6. package/dist/esm/index.d.mts.map +1 -1
  7. package/dist/esm/index.mjs +1 -1
  8. package/dist/esm/index.mjs.map +1 -1
  9. package/package.json +5 -6
  10. package/src/RouterProvider.tsx +0 -152
  11. package/src/components/Await.tsx +0 -99
  12. package/src/components/ClientOnly.tsx +0 -25
  13. package/src/components/HttpStatusCode.tsx +0 -82
  14. package/src/components/HttpStatusProvider.tsx +0 -22
  15. package/src/components/Link.tsx +0 -141
  16. package/src/components/RouteView/RouteView.tsx +0 -57
  17. package/src/components/RouteView/components.tsx +0 -19
  18. package/src/components/RouteView/helpers.tsx +0 -174
  19. package/src/components/RouteView/index.ts +0 -8
  20. package/src/components/RouteView/types.ts +0 -24
  21. package/src/components/RouterErrorBoundary.tsx +0 -84
  22. package/src/components/ServerOnly.tsx +0 -26
  23. package/src/components/Streamed.tsx +0 -24
  24. package/src/constants.ts +0 -9
  25. package/src/context.ts +0 -27
  26. package/src/hooks/useDeferred.tsx +0 -26
  27. package/src/hooks/useIsActiveRoute.tsx +0 -46
  28. package/src/hooks/useNavigator.tsx +0 -8
  29. package/src/hooks/useRoute.tsx +0 -26
  30. package/src/hooks/useRouteEnter.tsx +0 -147
  31. package/src/hooks/useRouteExit.tsx +0 -159
  32. package/src/hooks/useRouteNode.tsx +0 -34
  33. package/src/hooks/useRouteUtils.tsx +0 -12
  34. package/src/hooks/useRouter.tsx +0 -8
  35. package/src/hooks/useRouterTransition.tsx +0 -17
  36. package/src/index.ts +0 -56
  37. package/src/ssr.ts +0 -39
  38. package/src/types.ts +0 -40
  39. package/src/useSyncExternalStore.ts +0 -60
  40. 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.2",
3
+ "version": "0.15.4",
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.58.0",
67
67
  "@real-router/route-utils": "^0.2.3",
68
- "@real-router/core": "^0.56.0",
69
- "@real-router/sources": "^0.8.5"
68
+ "@real-router/sources": "^0.8.7"
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.6"
77
+ "@real-router/browser-plugin": "^0.17.8"
79
78
  },
80
79
  "peerDependencies": {
81
80
  "preact": ">=10.28.0 || ^11.0.0-0"
@@ -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
- };
@@ -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
- }
@@ -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";