@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.
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +2 -2
- package/dist/esm/index.d.mts.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
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { UNKNOWN_ROUTE } from "@real-router/core";
|
|
2
|
-
import { startsWithSegment } from "@real-router/route-utils";
|
|
3
|
-
import { Fragment, isValidElement, toChildArray } from "preact";
|
|
4
|
-
import { Suspense } from "preact/compat";
|
|
5
|
-
|
|
6
|
-
import { Match, NotFound, Self } from "./components";
|
|
7
|
-
|
|
8
|
-
import type { MatchProps, NotFoundProps, SelfProps } from "./types";
|
|
9
|
-
import type { VNode, ComponentChildren } from "preact";
|
|
10
|
-
|
|
11
|
-
const MARKER_TYPES: ReadonlySet<unknown> = new Set([Match, Self, NotFound]);
|
|
12
|
-
|
|
13
|
-
interface FallbackSlots {
|
|
14
|
-
selfChildren: ComponentChildren;
|
|
15
|
-
selfFallback: ComponentChildren | undefined;
|
|
16
|
-
selfFound: boolean;
|
|
17
|
-
notFoundChildren: ComponentChildren;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function isSegmentMatch(
|
|
21
|
-
routeName: string,
|
|
22
|
-
fullSegmentName: string,
|
|
23
|
-
exact: boolean,
|
|
24
|
-
): boolean {
|
|
25
|
-
if (fullSegmentName === "") {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (exact) {
|
|
30
|
-
return routeName === fullSegmentName;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return startsWithSegment(routeName, fullSegmentName);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function collectElements(
|
|
37
|
-
children: ComponentChildren,
|
|
38
|
-
result: VNode[],
|
|
39
|
-
): void {
|
|
40
|
-
for (const child of toChildArray(children)) {
|
|
41
|
-
if (!isValidElement(child)) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (MARKER_TYPES.has(child.type)) {
|
|
46
|
-
result.push(child);
|
|
47
|
-
} else {
|
|
48
|
-
collectElements(
|
|
49
|
-
(child.props as { readonly children: ComponentChildren }).children,
|
|
50
|
-
result,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function renderSlot(
|
|
57
|
-
slotChildren: ComponentChildren,
|
|
58
|
-
key: string,
|
|
59
|
-
fallback?: ComponentChildren,
|
|
60
|
-
): VNode {
|
|
61
|
-
const content =
|
|
62
|
-
fallback === undefined ? (
|
|
63
|
-
slotChildren
|
|
64
|
-
) : (
|
|
65
|
-
<Suspense fallback={fallback}>{slotChildren}</Suspense>
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
return <Fragment key={key}>{content}</Fragment>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function isFallbackKind(child: VNode): boolean {
|
|
72
|
-
return child.type === NotFound || child.type === Self;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function assignFallbackSlot(child: VNode, slots: FallbackSlots): void {
|
|
76
|
-
if (child.type === NotFound) {
|
|
77
|
-
slots.notFoundChildren = (child.props as NotFoundProps).children;
|
|
78
|
-
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (!slots.selfFound) {
|
|
83
|
-
slots.selfChildren = (child.props as SelfProps).children;
|
|
84
|
-
slots.selfFallback = (child.props as SelfProps).fallback;
|
|
85
|
-
slots.selfFound = true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function processMatch(
|
|
90
|
-
child: VNode,
|
|
91
|
-
routeName: string,
|
|
92
|
-
nodeName: string,
|
|
93
|
-
alreadyActive: boolean,
|
|
94
|
-
): VNode | null {
|
|
95
|
-
const {
|
|
96
|
-
segment,
|
|
97
|
-
exact = false,
|
|
98
|
-
fallback,
|
|
99
|
-
children,
|
|
100
|
-
} = child.props as MatchProps;
|
|
101
|
-
const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
|
|
102
|
-
const isActive =
|
|
103
|
-
!alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);
|
|
104
|
-
|
|
105
|
-
if (!isActive) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return renderSlot(children, fullSegmentName, fallback);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function appendFallback(
|
|
113
|
-
rendered: VNode[],
|
|
114
|
-
routeName: string,
|
|
115
|
-
nodeName: string,
|
|
116
|
-
slots: FallbackSlots,
|
|
117
|
-
): void {
|
|
118
|
-
if (slots.selfFound && routeName === nodeName) {
|
|
119
|
-
rendered.push(
|
|
120
|
-
renderSlot(slots.selfChildren, "__route-view-self__", slots.selfFallback),
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {
|
|
127
|
-
rendered.push(
|
|
128
|
-
<Fragment key="__route-view-not-found__">
|
|
129
|
-
{slots.notFoundChildren}
|
|
130
|
-
</Fragment>,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function buildRenderList(
|
|
136
|
-
elements: VNode[],
|
|
137
|
-
routeName: string,
|
|
138
|
-
nodeName: string,
|
|
139
|
-
): { rendered: VNode[]; activeMatchFound: boolean } {
|
|
140
|
-
const slots: FallbackSlots = {
|
|
141
|
-
selfChildren: null,
|
|
142
|
-
selfFallback: undefined,
|
|
143
|
-
selfFound: false,
|
|
144
|
-
notFoundChildren: null,
|
|
145
|
-
};
|
|
146
|
-
let activeMatchFound = false;
|
|
147
|
-
const rendered: VNode[] = [];
|
|
148
|
-
|
|
149
|
-
for (const child of elements) {
|
|
150
|
-
if (isFallbackKind(child)) {
|
|
151
|
-
assignFallbackSlot(child, slots);
|
|
152
|
-
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const matchRendered = processMatch(
|
|
157
|
-
child,
|
|
158
|
-
routeName,
|
|
159
|
-
nodeName,
|
|
160
|
-
activeMatchFound,
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
if (matchRendered !== null) {
|
|
164
|
-
activeMatchFound = true;
|
|
165
|
-
rendered.push(matchRendered);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (!activeMatchFound) {
|
|
170
|
-
appendFallback(rendered, routeName, nodeName, slots);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return { rendered, activeMatchFound };
|
|
174
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { ComponentChildren } from "preact";
|
|
2
|
-
|
|
3
|
-
export interface RouteViewProps {
|
|
4
|
-
readonly nodeName: string;
|
|
5
|
-
readonly children: ComponentChildren;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface MatchProps {
|
|
9
|
-
readonly segment: string;
|
|
10
|
-
readonly exact?: boolean;
|
|
11
|
-
readonly fallback?: ComponentChildren;
|
|
12
|
-
readonly children: ComponentChildren;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SelfProps {
|
|
16
|
-
/** Fallback content while children are suspended. */
|
|
17
|
-
readonly fallback?: ComponentChildren;
|
|
18
|
-
/** Content to render when the active route name equals the parent RouteView's nodeName. */
|
|
19
|
-
readonly children: ComponentChildren;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface NotFoundProps {
|
|
23
|
-
readonly children: ComponentChildren;
|
|
24
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { createDismissableError } from "@real-router/sources";
|
|
2
|
-
import { Fragment } from "preact";
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
|
4
|
-
|
|
5
|
-
import { useRouter } from "../hooks/useRouter";
|
|
6
|
-
import { useSyncExternalStore } from "../useSyncExternalStore";
|
|
7
|
-
|
|
8
|
-
import type { RouterError, State } from "@real-router/core";
|
|
9
|
-
import type { ComponentChildren, VNode } from "preact";
|
|
10
|
-
|
|
11
|
-
export interface RouterErrorBoundaryProps {
|
|
12
|
-
readonly children: ComponentChildren;
|
|
13
|
-
readonly fallback: (
|
|
14
|
-
error: RouterError,
|
|
15
|
-
resetError: () => void,
|
|
16
|
-
) => ComponentChildren;
|
|
17
|
-
readonly onError?: (
|
|
18
|
-
error: RouterError,
|
|
19
|
-
toRoute: State | null,
|
|
20
|
-
fromRoute: State | null,
|
|
21
|
-
) => void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Declarative navigation-error boundary.
|
|
26
|
-
*
|
|
27
|
-
* **Not** a Preact `componentDidCatch`-style ErrorBoundary — this component
|
|
28
|
-
* does NOT catch render-time exceptions from `children`. It is a compositional
|
|
29
|
-
* component that subscribes to `createDismissableError` from
|
|
30
|
-
* `@real-router/sources` and renders `fallback(error, resetError)` ALONGSIDE
|
|
31
|
-
* `children` (wrapped in a `<Fragment>`) when the router emits a navigation
|
|
32
|
-
* error (guard rejection, ROUTE_NOT_FOUND, etc.). The boundary auto-resets on
|
|
33
|
-
* the next successful navigation; `resetError()` lets the consumer dismiss
|
|
34
|
-
* the fallback imperatively.
|
|
35
|
-
*
|
|
36
|
-
* For real exception boundaries, wrap children in a Preact ErrorBoundary
|
|
37
|
-
* (e.g. `preact-iso/ErrorBoundary` or a custom `componentDidCatch` class) —
|
|
38
|
-
* the two can coexist.
|
|
39
|
-
*/
|
|
40
|
-
export function RouterErrorBoundary({
|
|
41
|
-
children,
|
|
42
|
-
fallback,
|
|
43
|
-
onError,
|
|
44
|
-
}: RouterErrorBoundaryProps): VNode {
|
|
45
|
-
const router = useRouter();
|
|
46
|
-
|
|
47
|
-
// `createDismissableError` is the cached factory from `@real-router/sources`
|
|
48
|
-
// — keyed per-router, identity stable across renders. `useMemo` would wrap
|
|
49
|
-
// a call that already memoizes downstream.
|
|
50
|
-
const store = createDismissableError(router);
|
|
51
|
-
const snapshot = useSyncExternalStore(
|
|
52
|
-
store.subscribe,
|
|
53
|
-
store.getSnapshot,
|
|
54
|
-
store.getSnapshot,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
const onErrorRef = useRef(onError);
|
|
58
|
-
|
|
59
|
-
useLayoutEffect(() => {
|
|
60
|
-
onErrorRef.current = onError;
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// snapshot.version is the @real-router/sources dismissable-error invariant:
|
|
64
|
-
// it is the only field that monotonically advances on each new error episode
|
|
65
|
-
// (snapshot.error/toRoute/fromRoute are correlated reads within the same
|
|
66
|
-
// version frame), so depending on it covers all error fields by construction.
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
if (snapshot.error) {
|
|
69
|
-
onErrorRef.current?.(
|
|
70
|
-
snapshot.error,
|
|
71
|
-
snapshot.toRoute,
|
|
72
|
-
snapshot.fromRoute,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
// eslint-disable-next-line @eslint-react/exhaustive-deps -- onError tracked via ref, snapshot fields accessed inside callback
|
|
76
|
-
}, [snapshot.version]);
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<Fragment>
|
|
80
|
-
{children}
|
|
81
|
-
{snapshot.error ? fallback(snapshot.error, snapshot.resetError) : null}
|
|
82
|
-
</Fragment>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
import type { ComponentChildren } from "preact";
|
|
4
|
-
|
|
5
|
-
export interface ServerOnlyProps {
|
|
6
|
-
readonly children: ComponentChildren;
|
|
7
|
-
readonly fallback?: ComponentChildren;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function ServerOnly({
|
|
11
|
-
children,
|
|
12
|
-
fallback = null,
|
|
13
|
-
}: ServerOnlyProps): ComponentChildren {
|
|
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
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { Suspense } from "preact/compat";
|
|
2
|
-
|
|
3
|
-
import type { ComponentChildren } from "preact";
|
|
4
|
-
|
|
5
|
-
export interface StreamedProps {
|
|
6
|
-
/** Shown while any descendant `<Await>` / `use(promise)`-equivalent suspends. */
|
|
7
|
-
readonly fallback: ComponentChildren;
|
|
8
|
-
readonly children: ComponentChildren;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Cross-adapter alias for `<Suspense fallback={…}>` from `preact/compat`.
|
|
13
|
-
* Pairs with `<Await>` for symmetry with the React/Solid/Svelte/Vue/Angular
|
|
14
|
-
* SSR streaming naming.
|
|
15
|
-
*
|
|
16
|
-
* Preact's `Suspense` is part of `preact/compat` (experimental). For
|
|
17
|
-
* production streaming the preact-render-to-string toolchain is required.
|
|
18
|
-
*/
|
|
19
|
-
export function Streamed({
|
|
20
|
-
fallback,
|
|
21
|
-
children,
|
|
22
|
-
}: StreamedProps): ComponentChildren {
|
|
23
|
-
return <Suspense fallback={fallback}>{children}</Suspense>;
|
|
24
|
-
}
|
package/src/constants.ts
DELETED
package/src/context.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createContext } from "preact";
|
|
2
|
-
import { useContext } from "preact/hooks";
|
|
3
|
-
|
|
4
|
-
import type { RouteContext as RouteContextType } from "./types";
|
|
5
|
-
import type { Router, Navigator } from "@real-router/core";
|
|
6
|
-
import type { Context } from "preact";
|
|
7
|
-
|
|
8
|
-
export const RouteContext = createContext<RouteContextType | null>(null);
|
|
9
|
-
|
|
10
|
-
export const RouterContext = createContext<Router | null>(null);
|
|
11
|
-
|
|
12
|
-
export const NavigatorContext = createContext<Navigator | null>(null);
|
|
13
|
-
|
|
14
|
-
export function createUseContextOrThrow<T>(
|
|
15
|
-
context: Context<T | null>,
|
|
16
|
-
hookName: string,
|
|
17
|
-
): () => T {
|
|
18
|
-
return () => {
|
|
19
|
-
const value = useContext(context);
|
|
20
|
-
|
|
21
|
-
if (!value) {
|
|
22
|
-
throw new Error(`${hookName} must be used within a RouterProvider`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return value;
|
|
26
|
-
};
|
|
27
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { useRoute } from "./useRoute";
|
|
2
|
-
|
|
3
|
-
interface DeferredContext {
|
|
4
|
-
ssrDataDeferred?: Record<string, Promise<unknown>>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const NEVER_PROMISE = new Promise<never>(() => {
|
|
8
|
-
// Intentionally never resolves — surfaces a forever-pending Suspense boundary
|
|
9
|
-
// when a key is requested that the loader never declared.
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
|
|
14
|
-
* inside an SSR data loader. Mirror of `@real-router/react/ssr` `useDeferred`
|
|
15
|
-
* — same `state.context.ssrDataDeferred` contract, same NEVER-on-missing
|
|
16
|
-
* fallback. Pair with `<Await>` (this package) which adds Preact-side
|
|
17
|
-
* promise-status tracking since Preact 10 has no `use(promise)` analogue.
|
|
18
|
-
*/
|
|
19
|
-
export function useDeferred<T = unknown>(key: string): Promise<T> {
|
|
20
|
-
const { route } = useRoute();
|
|
21
|
-
const context = route.context as DeferredContext;
|
|
22
|
-
const deferred = context.ssrDataDeferred;
|
|
23
|
-
const promise = deferred?.[key];
|
|
24
|
-
|
|
25
|
-
return (promise ?? NEVER_PROMISE) as Promise<T>;
|
|
26
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { createActiveRouteSource } from "@real-router/sources";
|
|
2
|
-
import { useMemo } from "preact/hooks";
|
|
3
|
-
|
|
4
|
-
import { useSyncExternalStore } from "../useSyncExternalStore";
|
|
5
|
-
import { useRouter } from "./useRouter";
|
|
6
|
-
|
|
7
|
-
import type { Params } from "@real-router/core";
|
|
8
|
-
import type { ActiveRouteSourceOptions } from "@real-router/sources";
|
|
9
|
-
|
|
10
|
-
export function useIsActiveRoute(
|
|
11
|
-
routeName: string,
|
|
12
|
-
params?: Params,
|
|
13
|
-
strict = false,
|
|
14
|
-
ignoreQueryParams = true,
|
|
15
|
-
hash?: string,
|
|
16
|
-
): boolean {
|
|
17
|
-
const router = useRouter();
|
|
18
|
-
|
|
19
|
-
// createActiveRouteSource is per-router + canonical-args cached in
|
|
20
|
-
// @real-router/sources. Caching the opts object + memoising the source
|
|
21
|
-
// lookup avoids (a) re-allocating the literal on every render and (b)
|
|
22
|
-
// re-running canonicalJson(params) on the cache lookup path. The `hash`
|
|
23
|
-
// argument (#532) participates in the cache key — a tab Link pointing to
|
|
24
|
-
// `/settings#account` shares its source only with consumers using the
|
|
25
|
-
// same routeName + params + hash. exactOptionalPropertyTypes forbids
|
|
26
|
-
// `{ hash: undefined }` literally, so we conditionally include the key
|
|
27
|
-
// only when the caller passed a value.
|
|
28
|
-
const opts = useMemo<ActiveRouteSourceOptions>(
|
|
29
|
-
() =>
|
|
30
|
-
hash === undefined
|
|
31
|
-
? { strict, ignoreQueryParams }
|
|
32
|
-
: { strict, ignoreQueryParams, hash },
|
|
33
|
-
[strict, ignoreQueryParams, hash],
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const store = useMemo(
|
|
37
|
-
() => createActiveRouteSource(router, routeName, params, opts),
|
|
38
|
-
[router, routeName, params, opts],
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
return useSyncExternalStore(
|
|
42
|
-
store.subscribe,
|
|
43
|
-
store.getSnapshot,
|
|
44
|
-
store.getSnapshot,
|
|
45
|
-
);
|
|
46
|
-
}
|
package/src/hooks/useRoute.tsx
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { createUseContextOrThrow, RouteContext } from "../context";
|
|
2
|
-
|
|
3
|
-
import type { RouteContext as RouteContextType } from "../types";
|
|
4
|
-
import type { Params, State } from "@real-router/core";
|
|
5
|
-
|
|
6
|
-
const useRouteContextOrThrow = createUseContextOrThrow(
|
|
7
|
-
RouteContext,
|
|
8
|
-
"useRoute",
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
export const useRoute = <P extends Params = Params>(): Omit<
|
|
12
|
-
RouteContextType<P>,
|
|
13
|
-
"route"
|
|
14
|
-
> & { route: State<P> } => {
|
|
15
|
-
const routeContext = useRouteContextOrThrow();
|
|
16
|
-
|
|
17
|
-
if (!routeContext.route) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
"useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return routeContext as Omit<RouteContextType<P>, "route"> & {
|
|
24
|
-
route: State<P>;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
import { useRoute } from "./useRoute";
|
|
4
|
-
|
|
5
|
-
import type { State } from "@real-router/core";
|
|
6
|
-
|
|
7
|
-
export interface RouteEnterContext {
|
|
8
|
-
/** The route that was just activated. */
|
|
9
|
-
route: State;
|
|
10
|
-
/** The route that was active immediately before this navigation. */
|
|
11
|
-
previousRoute: State;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type RouteEnterHandler = (context: RouteEnterContext) => void;
|
|
15
|
-
|
|
16
|
-
export interface UseRouteEnterOptions {
|
|
17
|
-
/**
|
|
18
|
-
* Skip the handler when `route.name === previousRoute.name`
|
|
19
|
-
* (sort/filter/query-only navigations on the same route). Default:
|
|
20
|
-
* `true`. Symmetric with `useRouteExit`'s same-name option.
|
|
21
|
-
*/
|
|
22
|
-
skipSameRoute?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Fire `handler` once when the component mounts as a result of a
|
|
27
|
-
* navigation. Mirror of `useRouteExit` for the entry side.
|
|
28
|
-
*
|
|
29
|
-
* What this hook covers that ad-hoc `useEffect` + `useRoute()` doesn't:
|
|
30
|
-
*
|
|
31
|
-
* - **Skip-initial**: handler is skipped when there is no
|
|
32
|
-
* `previousRoute` (i.e. first-load mount). Most consumers want to
|
|
33
|
-
* fire side effects only on real navigations, not on hydration.
|
|
34
|
-
* - **Same-route skip** (default): handler is skipped when
|
|
35
|
-
* `route.name === previousRoute.name`. Sort/filter/query-only
|
|
36
|
-
* navigations re-run the effect (because `route` reference changes
|
|
37
|
-
* in `useRoute`'s snapshot), but they are not "entries" in the
|
|
38
|
-
* animation / analytics sense — the component instance has stayed
|
|
39
|
-
* mounted throughout. Opt out with `skipSameRoute: false` when
|
|
40
|
-
* the handler legitimately needs to fire on every navigation
|
|
41
|
-
* (e.g. analytics tracking each query-param flip).
|
|
42
|
-
* - **Latest-handler ref**: the handler can change identity on every
|
|
43
|
-
* render without re-running the effect — the registered wrapper
|
|
44
|
-
* dispatches to whatever `handlerRef.current` points to.
|
|
45
|
-
* - **Mount-time `route` / `previousRoute` snapshot**: the handler
|
|
46
|
-
* receives the values that were live at the moment of mount, not
|
|
47
|
-
* the latest ones (which may have moved on if the user navigated
|
|
48
|
-
* again before the effect drained).
|
|
49
|
-
*
|
|
50
|
-
* Race-safety: `useRoute()` is wired through `useSyncExternalStore` from
|
|
51
|
-
* `@real-router/sources` (Preact polyfill: useState + useEffect, same
|
|
52
|
-
* post-commit semantics), so by the time the new component's effect
|
|
53
|
-
* runs, the snapshot is the post-commit one. This is the reason we can
|
|
54
|
-
* read mount-time context from `useRoute()` instead of subscribing to
|
|
55
|
-
* `router.subscribe` directly (which fires before Preact schedules a
|
|
56
|
-
* re-render — the well-known race in distributed components).
|
|
57
|
-
*
|
|
58
|
-
* Note: Preact does not expose a `StrictMode` equivalent, so the
|
|
59
|
-
* `lastHandledRouteRef` guard exists primarily for defensive symmetry
|
|
60
|
-
* with the React implementation. It is harmless in Preact.
|
|
61
|
-
*
|
|
62
|
-
* @example Direction-aware entry animation
|
|
63
|
-
* ```tsx
|
|
64
|
-
* useRouteEnter(({ route }) => {
|
|
65
|
-
* const direction = route.context.browser?.direction;
|
|
66
|
-
* ref.current?.classList.add(
|
|
67
|
-
* direction === "back" ? "slide-from-left" : "slide-from-right",
|
|
68
|
-
* );
|
|
69
|
-
* });
|
|
70
|
-
* ```
|
|
71
|
-
*
|
|
72
|
-
* @example Source-aware focus management
|
|
73
|
-
* ```tsx
|
|
74
|
-
* useRouteEnter(({ route }) => {
|
|
75
|
-
* if (route.context.browser?.source === "navigate") {
|
|
76
|
-
* headingRef.current?.focus();
|
|
77
|
-
* }
|
|
78
|
-
* });
|
|
79
|
-
* ```
|
|
80
|
-
*
|
|
81
|
-
* @example Analytics page-enter event (skip-initial built-in)
|
|
82
|
-
* ```tsx
|
|
83
|
-
* useRouteEnter(({ route, previousRoute }) => {
|
|
84
|
-
* analytics.track("page_enter", {
|
|
85
|
-
* route: route.name,
|
|
86
|
-
* from: previousRoute.name,
|
|
87
|
-
* });
|
|
88
|
-
* });
|
|
89
|
-
* ```
|
|
90
|
-
*
|
|
91
|
-
* @example Reading rich transition metadata via `route.transition`
|
|
92
|
-
* ```tsx
|
|
93
|
-
* useRouteEnter(({ route }) => {
|
|
94
|
-
* // route.transition: TransitionMeta — populated by core for every state
|
|
95
|
-
* if (route.transition.redirected) {
|
|
96
|
-
* showToast(`Redirected from ${route.transition.from}`);
|
|
97
|
-
* }
|
|
98
|
-
* if (route.transition.segments.activated.includes("products")) {
|
|
99
|
-
* // products subtree just became active (could be products or
|
|
100
|
-
* // products.detail). Useful for subtree-scoped side effects.
|
|
101
|
-
* }
|
|
102
|
-
* });
|
|
103
|
-
* ```
|
|
104
|
-
*/
|
|
105
|
-
export function useRouteEnter(
|
|
106
|
-
handler: RouteEnterHandler,
|
|
107
|
-
options?: UseRouteEnterOptions,
|
|
108
|
-
): void {
|
|
109
|
-
const { route, previousRoute } = useRoute();
|
|
110
|
-
const handlerRef = useRef(handler);
|
|
111
|
-
const lastHandledRouteRef = useRef<State | null>(null);
|
|
112
|
-
const skipSameRoute = options?.skipSameRoute ?? true;
|
|
113
|
-
|
|
114
|
-
// Keep the latest handler reference accessible without re-running
|
|
115
|
-
// the effect. useLayoutEffect (synchronous, post-render, pre-paint)
|
|
116
|
-
// updates the ref before the effect can read it.
|
|
117
|
-
useLayoutEffect(() => {
|
|
118
|
-
handlerRef.current = handler;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
// Early-exit guards, top-down:
|
|
123
|
-
//
|
|
124
|
-
// - **Skip-initial**: `state.transition.from` is undefined only
|
|
125
|
-
// for the very first state committed by `router.start()`.
|
|
126
|
-
// - **Skip-same-route**: query-only navigations have
|
|
127
|
-
// `transition.from === route.name`. Opt-out via
|
|
128
|
-
// `skipSameRoute: false`.
|
|
129
|
-
// - **Defensive dedupe**: same `route` ref between effect
|
|
130
|
-
// cleanup + re-run. Preact has no StrictMode, but we keep the
|
|
131
|
-
// guard for parity with React; v8-ignored.
|
|
132
|
-
if (!route.transition.from) {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
if (skipSameRoute && route.transition.from === route.name) {
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
/* v8 ignore start */
|
|
139
|
-
if (lastHandledRouteRef.current === route || !previousRoute) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
/* v8 ignore stop */
|
|
143
|
-
|
|
144
|
-
lastHandledRouteRef.current = route;
|
|
145
|
-
handlerRef.current({ route, previousRoute });
|
|
146
|
-
}, [route, previousRoute, skipSameRoute]);
|
|
147
|
-
}
|