@real-router/preact 0.15.1 → 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.
Files changed (42) hide show
  1. package/dist/cjs/index.d.ts.map +1 -1
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/ssr.d.ts.map +1 -1
  5. package/dist/cjs/ssr.js.map +1 -1
  6. package/dist/cjs/useRoute-B0pDCgZw.js.map +1 -1
  7. package/dist/esm/index.d.mts.map +1 -1
  8. package/dist/esm/index.mjs +1 -1
  9. package/dist/esm/index.mjs.map +1 -1
  10. package/dist/esm/ssr.d.mts.map +1 -1
  11. package/package.json +6 -7
  12. package/src/RouterProvider.tsx +0 -152
  13. package/src/components/Await.tsx +0 -99
  14. package/src/components/ClientOnly.tsx +0 -25
  15. package/src/components/HttpStatusCode.tsx +0 -82
  16. package/src/components/HttpStatusProvider.tsx +0 -22
  17. package/src/components/Link.tsx +0 -141
  18. package/src/components/RouteView/RouteView.tsx +0 -57
  19. package/src/components/RouteView/components.tsx +0 -19
  20. package/src/components/RouteView/helpers.tsx +0 -176
  21. package/src/components/RouteView/index.ts +0 -8
  22. package/src/components/RouteView/types.ts +0 -24
  23. package/src/components/RouterErrorBoundary.tsx +0 -84
  24. package/src/components/ServerOnly.tsx +0 -26
  25. package/src/components/Streamed.tsx +0 -24
  26. package/src/constants.ts +0 -9
  27. package/src/context.ts +0 -27
  28. package/src/hooks/useDeferred.tsx +0 -26
  29. package/src/hooks/useIsActiveRoute.tsx +0 -46
  30. package/src/hooks/useNavigator.tsx +0 -8
  31. package/src/hooks/useRoute.tsx +0 -26
  32. package/src/hooks/useRouteEnter.tsx +0 -147
  33. package/src/hooks/useRouteExit.tsx +0 -159
  34. package/src/hooks/useRouteNode.tsx +0 -34
  35. package/src/hooks/useRouteUtils.tsx +0 -12
  36. package/src/hooks/useRouter.tsx +0 -8
  37. package/src/hooks/useRouterTransition.tsx +0 -17
  38. package/src/index.ts +0 -56
  39. package/src/ssr.ts +0 -39
  40. package/src/types.ts +0 -40
  41. package/src/useSyncExternalStore.ts +0 -60
  42. package/src/utils/createHttpStatusSink.ts +0 -27
@@ -1,176 +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
- interface FallbackSlots {
12
- selfChildren: ComponentChildren;
13
- selfFallback: ComponentChildren | undefined;
14
- selfFound: boolean;
15
- notFoundChildren: ComponentChildren;
16
- }
17
-
18
- function isSegmentMatch(
19
- routeName: string,
20
- fullSegmentName: string,
21
- exact: boolean,
22
- ): boolean {
23
- if (fullSegmentName === "") {
24
- return false;
25
- }
26
-
27
- if (exact) {
28
- return routeName === fullSegmentName;
29
- }
30
-
31
- return startsWithSegment(routeName, fullSegmentName);
32
- }
33
-
34
- export function collectElements(
35
- children: ComponentChildren,
36
- result: VNode[],
37
- ): void {
38
- for (const child of toChildArray(children)) {
39
- if (!isValidElement(child)) {
40
- continue;
41
- }
42
-
43
- if (
44
- child.type === Match ||
45
- child.type === Self ||
46
- child.type === NotFound
47
- ) {
48
- result.push(child);
49
- } else {
50
- collectElements(
51
- (child.props as { readonly children: ComponentChildren }).children,
52
- result,
53
- );
54
- }
55
- }
56
- }
57
-
58
- function renderSlot(
59
- slotChildren: ComponentChildren,
60
- key: string,
61
- fallback?: ComponentChildren,
62
- ): VNode {
63
- const content =
64
- fallback === undefined ? (
65
- slotChildren
66
- ) : (
67
- <Suspense fallback={fallback}>{slotChildren}</Suspense>
68
- );
69
-
70
- return <Fragment key={key}>{content}</Fragment>;
71
- }
72
-
73
- function isFallbackKind(child: VNode): boolean {
74
- return child.type === NotFound || child.type === Self;
75
- }
76
-
77
- function assignFallbackSlot(child: VNode, slots: FallbackSlots): void {
78
- if (child.type === NotFound) {
79
- slots.notFoundChildren = (child.props as NotFoundProps).children;
80
-
81
- return;
82
- }
83
-
84
- if (!slots.selfFound) {
85
- slots.selfChildren = (child.props as SelfProps).children;
86
- slots.selfFallback = (child.props as SelfProps).fallback;
87
- slots.selfFound = true;
88
- }
89
- }
90
-
91
- function processMatch(
92
- child: VNode,
93
- routeName: string,
94
- nodeName: string,
95
- alreadyActive: boolean,
96
- ): VNode | null {
97
- const {
98
- segment,
99
- exact = false,
100
- fallback,
101
- children,
102
- } = child.props as MatchProps;
103
- const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
104
- const isActive =
105
- !alreadyActive && isSegmentMatch(routeName, fullSegmentName, exact);
106
-
107
- if (!isActive) {
108
- return null;
109
- }
110
-
111
- return renderSlot(children, fullSegmentName, fallback);
112
- }
113
-
114
- function appendFallback(
115
- rendered: VNode[],
116
- routeName: string,
117
- nodeName: string,
118
- slots: FallbackSlots,
119
- ): void {
120
- if (slots.selfFound && routeName === nodeName) {
121
- rendered.push(
122
- renderSlot(slots.selfChildren, "__route-view-self__", slots.selfFallback),
123
- );
124
-
125
- return;
126
- }
127
-
128
- if (routeName === UNKNOWN_ROUTE && slots.notFoundChildren !== null) {
129
- rendered.push(
130
- <Fragment key="__route-view-not-found__">
131
- {slots.notFoundChildren}
132
- </Fragment>,
133
- );
134
- }
135
- }
136
-
137
- export function buildRenderList(
138
- elements: VNode[],
139
- routeName: string,
140
- nodeName: string,
141
- ): { rendered: VNode[]; activeMatchFound: boolean } {
142
- const slots: FallbackSlots = {
143
- selfChildren: null,
144
- selfFallback: undefined,
145
- selfFound: false,
146
- notFoundChildren: null,
147
- };
148
- let activeMatchFound = false;
149
- const rendered: VNode[] = [];
150
-
151
- for (const child of elements) {
152
- if (isFallbackKind(child)) {
153
- assignFallbackSlot(child, slots);
154
-
155
- continue;
156
- }
157
-
158
- const matchRendered = processMatch(
159
- child,
160
- routeName,
161
- nodeName,
162
- activeMatchFound,
163
- );
164
-
165
- if (matchRendered !== null) {
166
- activeMatchFound = true;
167
- rendered.push(matchRendered);
168
- }
169
- }
170
-
171
- if (!activeMatchFound) {
172
- appendFallback(rendered, routeName, nodeName, slots);
173
- }
174
-
175
- return { rendered, activeMatchFound };
176
- }
@@ -1,8 +0,0 @@
1
- export { RouteView } from "./RouteView";
2
-
3
- export type {
4
- RouteViewProps,
5
- RouteViewMatchProps,
6
- RouteViewSelfProps,
7
- RouteViewNotFoundProps,
8
- } from "./RouteView";
@@ -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
@@ -1,9 +0,0 @@
1
- /**
2
- * Stable empty object for default params
3
- */
4
- export const EMPTY_PARAMS = Object.freeze({});
5
-
6
- /**
7
- * Stable empty options object
8
- */
9
- export const EMPTY_OPTIONS = Object.freeze({});
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
- }
@@ -1,8 +0,0 @@
1
- import { createUseContextOrThrow, NavigatorContext } from "../context";
2
-
3
- import type { Navigator } from "@real-router/core";
4
-
5
- export const useNavigator: () => Navigator = createUseContextOrThrow(
6
- NavigatorContext,
7
- "useNavigator",
8
- );
@@ -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
- }