@real-router/solid 0.14.1 → 0.14.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 (41) hide show
  1. package/dist/cjs/index.js +0 -0
  2. package/dist/esm/index.mjs +0 -0
  3. package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
  4. package/dist/types/dom-utils/scroll-spy.d.ts.map +1 -1
  5. package/package.json +9 -10
  6. package/src/RouterProvider.tsx +0 -112
  7. package/src/components/Await.tsx +0 -56
  8. package/src/components/ClientOnly.tsx +0 -20
  9. package/src/components/HttpStatusCode.tsx +0 -65
  10. package/src/components/HttpStatusProvider.tsx +0 -21
  11. package/src/components/Link.tsx +0 -140
  12. package/src/components/RouteView/RouteView.tsx +0 -59
  13. package/src/components/RouteView/components.tsx +0 -85
  14. package/src/components/RouteView/helpers.tsx +0 -0
  15. package/src/components/RouteView/index.ts +0 -8
  16. package/src/components/RouteView/types.ts +0 -24
  17. package/src/components/RouterErrorBoundary.tsx +0 -45
  18. package/src/components/ServerOnly.tsx +0 -20
  19. package/src/components/Streamed.tsx +0 -23
  20. package/src/constants.ts +0 -27
  21. package/src/context.ts +0 -35
  22. package/src/createSignalFromSource.ts +0 -71
  23. package/src/createStoreFromSource.ts +0 -65
  24. package/src/directives/link.tsx +0 -111
  25. package/src/directives.d.ts +0 -10
  26. package/src/hooks/useDeferred.tsx +0 -36
  27. package/src/hooks/useNavigator.tsx +0 -6
  28. package/src/hooks/useRoute.tsx +0 -27
  29. package/src/hooks/useRouteEnter.tsx +0 -121
  30. package/src/hooks/useRouteExit.tsx +0 -123
  31. package/src/hooks/useRouteNode.tsx +0 -13
  32. package/src/hooks/useRouteNodeStore.tsx +0 -12
  33. package/src/hooks/useRouteStore.tsx +0 -12
  34. package/src/hooks/useRouteUtils.tsx +0 -50
  35. package/src/hooks/useRouter.tsx +0 -6
  36. package/src/hooks/useRouterTransition.tsx +0 -14
  37. package/src/index.tsx +0 -66
  38. package/src/ssr.tsx +0 -39
  39. package/src/types.ts +0 -28
  40. package/src/utils/createHttpStatusSink.ts +0 -31
  41. package/src/utils/createMountedSignal.ts +0 -26
@@ -1,45 +0,0 @@
1
- import { createDismissableError } from "@real-router/sources";
2
- import { createEffect, Show } from "solid-js";
3
-
4
- import { createSignalFromSource } from "../createSignalFromSource";
5
- import { useRouter } from "../hooks/useRouter";
6
-
7
- import type { RouterError, State } from "@real-router/core";
8
- import type { JSX } from "solid-js";
9
-
10
- export interface RouterErrorBoundaryProps {
11
- readonly children: JSX.Element;
12
- readonly fallback: (
13
- error: RouterError,
14
- resetError: () => void,
15
- ) => JSX.Element;
16
- readonly onError?: (
17
- error: RouterError,
18
- toRoute: State | null,
19
- fromRoute: State | null,
20
- ) => void;
21
- }
22
-
23
- export function RouterErrorBoundary(
24
- props: RouterErrorBoundaryProps,
25
- ): JSX.Element {
26
- const router = useRouter();
27
- const snapshot = createSignalFromSource(createDismissableError(router));
28
-
29
- createEffect(() => {
30
- const snap = snapshot();
31
-
32
- if (snap.error) {
33
- props.onError?.(snap.error, snap.toRoute, snap.fromRoute);
34
- }
35
- });
36
-
37
- return (
38
- <>
39
- {props.children}
40
- <Show when={snapshot().error}>
41
- {(error) => props.fallback(error(), snapshot().resetError)}
42
- </Show>
43
- </>
44
- );
45
- }
@@ -1,20 +0,0 @@
1
- import { Show } from "solid-js";
2
-
3
- import { createMountedSignal } from "../utils/createMountedSignal";
4
-
5
- import type { JSX } from "solid-js";
6
-
7
- export interface ServerOnlyProps {
8
- readonly children: JSX.Element;
9
- readonly fallback?: JSX.Element;
10
- }
11
-
12
- export function ServerOnly(props: ServerOnlyProps): JSX.Element {
13
- const mounted = createMountedSignal();
14
-
15
- return (
16
- <Show when={mounted()} fallback={props.children}>
17
- {props.fallback}
18
- </Show>
19
- );
20
- }
@@ -1,23 +0,0 @@
1
- import { Suspense } from "solid-js";
2
-
3
- import type { JSX } from "solid-js";
4
-
5
- export interface StreamedProps {
6
- /** Shown while any descendant `<Await>` / `createResource` suspends. */
7
- readonly fallback: JSX.Element;
8
- readonly children: JSX.Element;
9
- }
10
-
11
- /**
12
- * Cross-adapter alias for Solid's `<Suspense fallback={…}>`. Symmetric naming
13
- * with the React/Preact/Svelte/Vue/Angular `<Streamed>` components — pick
14
- * `<Streamed>` for cross-framework consistency, or use Solid's native
15
- * `<Suspense>` directly when team conventions prefer that.
16
- *
17
- * Solid's `<Suspense>` is a built-in primitive; out-of-order resolution +
18
- * splice scripts during `renderToStream` are part of the runtime. See
19
- * Solid's SSR docs for the wire-format details.
20
- */
21
- export function Streamed(props: StreamedProps): JSX.Element {
22
- return <Suspense fallback={props.fallback}>{props.children}</Suspense>;
23
- }
package/src/constants.ts DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * Stable empty object for default params.
3
- *
4
- * `Object.freeze` makes mutation throw under ESM strict mode — this guards
5
- * against accidental writes that would corrupt the shared default across
6
- * every Link without explicit params.
7
- *
8
- * §8.1 audit note (LOW #19): consumers cast `EMPTY_PARAMS as P` at usage
9
- * sites (e.g. `Link.tsx`, `directives/link.tsx`). The cast is required for
10
- * type compatibility with the generic `P extends Params` and DOES technically
11
- * widen the `Readonly<{}>` type, but the underlying object stays frozen at
12
- * runtime — any attempt to mutate fails at the JS engine level regardless
13
- * of TS-level visibility. The frozen sentinel is also used by Link's
14
- * fast-path identity check (`props.routeParams === undefined` after the
15
- * §8.1 audit fix); changing this object's identity would silently break
16
- * that path.
17
- */
18
- export const EMPTY_PARAMS = Object.freeze({});
19
-
20
- /**
21
- * Stable empty options object.
22
- *
23
- * Same freeze/cast guarantees as `EMPTY_PARAMS` — the sentinel is shared
24
- * across all default `routeOptions` consumers (`Link`, `use:link`) to
25
- * avoid per-render `{}` allocations.
26
- */
27
- export const EMPTY_OPTIONS = Object.freeze({});
package/src/context.ts DELETED
@@ -1,35 +0,0 @@
1
- import { createContext, useContext } from "solid-js";
2
-
3
- import type { RouteState } from "./types";
4
- import type { Router, Navigator } from "@real-router/core";
5
- import type { Accessor } from "solid-js";
6
-
7
- export interface RouterContextValue {
8
- router: Router;
9
- navigator: Navigator;
10
- routeSelector: (routeName: string) => boolean;
11
- }
12
-
13
- export const RouterContext = createContext<RouterContextValue | null>(null);
14
-
15
- export const RouteContext = createContext<Accessor<RouteState> | null>(null);
16
-
17
- /**
18
- * Read the required RouterContext or throw a labelled error. Internal helper
19
- * — consolidates 4 copies of the same `useContext + null-check + throw`
20
- * block across the public hooks/components/directives. The `consumerName`
21
- * parameter keeps each callsite's error message specific (so the consumer
22
- * sees "useRouter must be used within a RouterProvider", not a generic
23
- * "context missing" message).
24
- */
25
- export function useRequiredRouterContext(
26
- consumerName: string,
27
- ): RouterContextValue {
28
- const ctx = useContext(RouterContext);
29
-
30
- if (!ctx) {
31
- throw new Error(`${consumerName} must be used within a RouterProvider`);
32
- }
33
-
34
- return ctx;
35
- }
@@ -1,71 +0,0 @@
1
- import { createSignal, onCleanup } from "solid-js";
2
-
3
- import type { RouterSource } from "@real-router/sources";
4
- import type { Accessor } from "solid-js";
5
-
6
- export function createSignalFromSource<T>(
7
- source: RouterSource<T>,
8
- ): Accessor<T> {
9
- // Mini-sprint E.5 (audit-5 §4.2 #7) — defensive init-phase snapshot
10
- // reads. A throwing `getSnapshot()` during construction would
11
- // propagate up through `createSignal<T>(...)` (or the post-subscribe
12
- // re-sync below) into the reactive owner, tearing down the entire
13
- // RouterProvider subtree (and any siblings sharing the owner). Catch
14
- // + log + fall back to `undefined` (initial) or skip-update (post-
15
- // subscribe re-sync) so the accessor still constructs; the next
16
- // emit refreshes the value.
17
- //
18
- // Post-init emit-time throws are NOT wrapped — they bubble to Solid's
19
- // `<ErrorBoundary>` (or surface as unhandled errors in dev) so
20
- // genuine source bugs aren't silently masked.
21
- let initial: T;
22
-
23
- try {
24
- initial = source.getSnapshot();
25
- } catch (error) {
26
- console.error(
27
- "[real-router] createSignalFromSource: initial getSnapshot threw — accessor defaulting to undefined.",
28
- error,
29
- );
30
- initial = undefined as T;
31
- }
32
-
33
- const [value, setValue] = createSignal<T>(initial);
34
-
35
- // `sync` is a stable reference (defined once at outer scope) so the
36
- // subscribe callback below does not re-allocate it per emit. Solid's
37
- // `setValue(fn)` treats fn as an updater `(prev) => next`; our updater
38
- // ignores `prev` and reads the latest snapshot fresh, which gives a
39
- // function-form micro-allocation cost (one extra fn call per emit) BUT
40
- // a much smaller TS surface than the `setValue(value)` direct form —
41
- // that overload is typed `Exclude<T, Function>`, requiring per-call
42
- // `as Exclude<T, (...args: never[]) => unknown>` casts for generic T.
43
- // The micro-opt is not worth the cast complexity.
44
- // See §8.2 audit note.
45
- const sync = (): T => source.getSnapshot();
46
-
47
- const unsubscribe = source.subscribe(() => {
48
- setValue(sync);
49
- });
50
-
51
- // Re-read after subscribe: lazy sources reconcile their snapshot in
52
- // onFirstSubscribe (when reused after disconnect via cache). Listener
53
- // is not notified for that internal update, so we must sync manually.
54
- // No-op when snapshot is unchanged (signal equality check). Wrapped
55
- // because this is still init-phase: a throw here ALSO tears down the
56
- // owner, same as the initial read above.
57
- try {
58
- setValue(sync);
59
- } catch (error) {
60
- console.error(
61
- "[real-router] createSignalFromSource: post-subscribe getSnapshot threw — accessor retains initial value.",
62
- error,
63
- );
64
- }
65
-
66
- onCleanup(() => {
67
- unsubscribe();
68
- });
69
-
70
- return value;
71
- }
@@ -1,65 +0,0 @@
1
- import { onCleanup } from "solid-js";
2
- import { createStore, reconcile } from "solid-js/store";
3
-
4
- import type { RouterSource } from "@real-router/sources";
5
-
6
- /**
7
- * Bridges a `RouterSource<T>` into a Solid store (`createStore` + `reconcile`).
8
- *
9
- * Unlike `createSignalFromSource` (whole-value replacement via `===`), this
10
- * bridge uses `reconcile` on every emit so **unchanged nested paths retain
11
- * their object identity**. Components that read only `state.route.name` will
12
- * not re-run when `state.route.params` changes — granular reactivity without
13
- * manual memoisation.
14
- *
15
- * **Ownership**: calls `onCleanup` — must be called inside a reactive owner
16
- * (component body or `createRoot`). Same contract as `createSignalFromSource`.
17
- *
18
- * **Lazy-source re-sync**: after `source.subscribe()`, a cached lazy source
19
- * may reconcile its snapshot in `onFirstSubscribe`. The listener is not
20
- * notified for that internal update, so we re-read immediately after
21
- * subscribing (`setState(reconcile(source.getSnapshot()))`) — mirrors the
22
- * same pattern in `createSignalFromSource`. `reconcile` is a no-op when the
23
- * snapshot is structurally unchanged, so there is no spurious reactivity cost.
24
- */
25
- export function createStoreFromSource<T extends object>(
26
- source: RouterSource<T>,
27
- ): T {
28
- const initialSnapshot = source.getSnapshot();
29
- const [state, setState] = createStore<T>({ ...initialSnapshot });
30
-
31
- // Track the last reconciled snapshot reference to short-circuit redundant
32
- // `reconcile` calls. Cached lazy sources (e.g. `createRouteNodeSource`)
33
- // stabilize their snapshot — the same reference flows through multiple
34
- // emits when nothing in the node's slice changed. `reconcile` itself
35
- // handles identity (no-ops on structurally-equal input), but a reference
36
- // check is cheaper than the structural walk and avoids the function call
37
- // entirely on every navigation × N store consumers (§8b H10 audit fix).
38
- let lastSnapshot: T = initialSnapshot;
39
-
40
- const unsubscribe = source.subscribe(() => {
41
- const nextSnapshot = source.getSnapshot();
42
-
43
- if (nextSnapshot === lastSnapshot) {
44
- return;
45
- }
46
-
47
- lastSnapshot = nextSnapshot;
48
- setState(reconcile(nextSnapshot));
49
- });
50
-
51
- // Re-read after subscribe: lazy sources reconcile their snapshot in
52
- // onFirstSubscribe (when reused after disconnect via cache). The listener
53
- // is not notified for that internal update, so we must reconcile manually.
54
- // Guarded by the same reference check so a no-op stays free.
55
- const afterSubscribe = source.getSnapshot();
56
-
57
- if (afterSubscribe !== lastSnapshot) {
58
- lastSnapshot = afterSubscribe;
59
- setState(reconcile(afterSubscribe));
60
- }
61
-
62
- onCleanup(unsubscribe);
63
-
64
- return state;
65
- }
@@ -1,111 +0,0 @@
1
- import { createActiveRouteSource } from "@real-router/sources";
2
- import { createEffect, onCleanup } from "solid-js";
3
-
4
- import { EMPTY_PARAMS, EMPTY_OPTIONS } from "../constants";
5
- import { createSignalFromSource } from "../createSignalFromSource";
6
- import { shouldNavigate, applyLinkA11y, buildHref } from "../dom-utils";
7
- import { useRouter } from "../hooks/useRouter";
8
-
9
- import type { Params } from "@real-router/core";
10
-
11
- export interface LinkDirectiveOptions<P extends Params = Params> {
12
- routeName: string;
13
- routeParams?: P;
14
- routeOptions?: Record<string, unknown>;
15
- activeClassName?: string;
16
- activeStrict?: boolean;
17
- ignoreQueryParams?: boolean;
18
- }
19
-
20
- export function link<P extends Params = Params>(
21
- element: HTMLElement,
22
- accessor: () => LinkDirectiveOptions<P>,
23
- ): void {
24
- const router = useRouter();
25
- const options = accessor();
26
-
27
- // audit-2026-05-17 §8a cleanup — single instanceof probe, single EMPTY_PARAMS
28
- // default. Previously evaluated three times for the <a>-only branches and
29
- // twice for routeParams. The directive accessor is read once at init
30
- // (documented "use:link Options Are Captured Once"), so both lookups are
31
- // stable and worth hoisting.
32
- const anchor = element instanceof HTMLAnchorElement ? element : null;
33
- const resolvedRouteParams = (options.routeParams ?? EMPTY_PARAMS) as P;
34
- const resolvedRouteOptions = options.routeOptions ?? EMPTY_OPTIONS;
35
-
36
- // Set href on <a> elements
37
- if (anchor) {
38
- const href = buildHref(router, options.routeName, resolvedRouteParams);
39
-
40
- if (href === undefined) {
41
- anchor.removeAttribute("href");
42
- } else {
43
- anchor.href = href;
44
- }
45
- }
46
-
47
- applyLinkA11y(element);
48
-
49
- // Active class tracking: only `isActive` is reactive (createEffect toggles
50
- // the class on each emit). The `options` object itself is captured ONCE at
51
- // init (see gotcha "use:link Options Are Captured Once") — changing
52
- // `activeClassName` / `routeName` / `routeParams` later has no effect.
53
- if (options.activeClassName) {
54
- const activeClassName = options.activeClassName;
55
- const activeSource = createActiveRouteSource(
56
- router,
57
- options.routeName,
58
- resolvedRouteParams,
59
- {
60
- strict: options.activeStrict ?? false,
61
- ignoreQueryParams: options.ignoreQueryParams ?? true,
62
- },
63
- );
64
- const isActive = createSignalFromSource(activeSource);
65
-
66
- createEffect(() => {
67
- element.classList.toggle(activeClassName, isActive());
68
- });
69
- }
70
-
71
- // Click handler
72
- function handleClick(evt: MouseEvent) {
73
- if (!shouldNavigate(evt)) {
74
- return;
75
- }
76
-
77
- // Mini-sprint E.2 (audit-5 §4.2 #2) — respect upstream
78
- // preventDefault. `<Link>` checks `local.onClick(evt); if
79
- // (evt.defaultPrevented) return;` because it owns the React-style
80
- // onClick prop. The directive has no equivalent prop, but the
81
- // consumer may register their OWN click listener on the same
82
- // element (DOM event order is "addEventListener queue, in
83
- // registration order"). If their listener called preventDefault
84
- // to opt-out of navigation, the directive must honour that.
85
- if (evt.defaultPrevented) {
86
- return;
87
- }
88
-
89
- // Symmetric with <Link> (#P0.6 audit): on an <a target="_blank"> the
90
- // browser opens the URL in a new tab/window natively. Intercepting the
91
- // click via preventDefault + router.navigate would suppress the new
92
- // tab and silently keep the user on the current page.
93
- if (anchor?.target === "_blank") {
94
- return;
95
- }
96
-
97
- if (anchor) {
98
- evt.preventDefault();
99
- }
100
-
101
- router
102
- .navigate(options.routeName, resolvedRouteParams, resolvedRouteOptions)
103
- .catch(() => {});
104
- }
105
-
106
- element.addEventListener("click", handleClick);
107
-
108
- onCleanup(() => {
109
- element.removeEventListener("click", handleClick);
110
- });
111
- }
@@ -1,10 +0,0 @@
1
- import type { LinkDirectiveOptions } from "./directives/link";
2
-
3
- declare module "solid-js" {
4
- // eslint-disable-next-line @typescript-eslint/no-namespace
5
- namespace JSX {
6
- interface Directives {
7
- link: LinkDirectiveOptions | undefined;
8
- }
9
- }
10
- }
@@ -1,36 +0,0 @@
1
- import { useRoute } from "./useRoute";
2
-
3
- import type { Accessor } from "solid-js";
4
-
5
- interface DeferredContext {
6
- ssrDataDeferred?: Record<string, Promise<unknown>>;
7
- }
8
-
9
- const NEVER_PROMISE = new Promise<never>(() => {
10
- // Intentionally never resolves — surfaces a forever-pending Suspense boundary
11
- // when a key is requested that the loader never declared.
12
- });
13
-
14
- /**
15
- * Read a deferred promise published by `defer({ deferred: { <key>: Promise } })`
16
- * inside an SSR data loader.
17
- *
18
- * Returns a Solid `Accessor<Promise<T>>` so the value tracks the active route
19
- * — re-reading on navigation picks up the new state's deferred map. Wrap with
20
- * `<Await name="key">{(value) => …}</Await>` (this package), which builds on
21
- * `createResource` + `<Suspense>` for native Solid streaming.
22
- *
23
- * Returns a forever-pending promise when the key is missing — surfaces
24
- * loader/consumer key drift as a visible Suspense fallback rather than a
25
- * silent runtime error.
26
- */
27
- export function useDeferred<T = unknown>(key: string): Accessor<Promise<T>> {
28
- const routeAccessor = useRoute();
29
-
30
- return () => {
31
- const context = routeAccessor().route.context as DeferredContext;
32
- const deferred = context.ssrDataDeferred;
33
-
34
- return (deferred?.[key] ?? NEVER_PROMISE) as Promise<T>;
35
- };
36
- }
@@ -1,6 +0,0 @@
1
- import { useRequiredRouterContext } from "../context";
2
-
3
- import type { Navigator } from "@real-router/core";
4
-
5
- export const useNavigator = (): Navigator =>
6
- useRequiredRouterContext("useNavigator").navigator;
@@ -1,27 +0,0 @@
1
- import { useContext } from "solid-js";
2
-
3
- import { RouteContext } from "../context";
4
-
5
- import type { RouteState } from "../types";
6
- import type { Params, State } from "@real-router/core";
7
- import type { Accessor } from "solid-js";
8
-
9
- export const useRoute = <P extends Params = Params>(): Accessor<
10
- Omit<RouteState<P>, "route"> & { route: State<P> }
11
- > => {
12
- const routeSignal = useContext(RouteContext);
13
-
14
- if (!routeSignal) {
15
- throw new Error("useRoute must be used within a RouterProvider");
16
- }
17
-
18
- if (!routeSignal().route) {
19
- throw new Error(
20
- "useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
21
- );
22
- }
23
-
24
- return routeSignal as Accessor<
25
- Omit<RouteState<P>, "route"> & { route: State<P> }
26
- >;
27
- };
@@ -1,121 +0,0 @@
1
- import { createEffect } from "solid-js";
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 an ad-hoc `createEffect` + `useRoute()`
30
- * doesn't:
31
- *
32
- * - **Skip-initial**: handler is skipped when there is no
33
- * `transition.from` (i.e. first-load mount). Most consumers want to
34
- * fire side effects only on real navigations, not on hydration.
35
- * - **Same-route skip** (default): handler is skipped when
36
- * `route.transition.from === route.name`. Sort/filter/query-only
37
- * navigations re-trigger the effect (because the `route` reference
38
- * changes), but they are not "entries" in the animation / analytics
39
- * sense — the component instance has stayed mounted throughout.
40
- * Opt out with `skipSameRoute: false`.
41
- * - **Mount-time `route` / `previousRoute` snapshot**: the handler
42
- * receives the values that were live at the moment of effect
43
- * activation, not the latest ones (which may have moved on if the
44
- * user navigated again before the effect drained).
45
- *
46
- * **Handler reactivity (Solid)**: Solid components run **once** at mount;
47
- * `handler` is captured in closure when the hook is called. If you need
48
- * different behavior across renders, derive it from a signal inside the
49
- * handler body — do not rely on swapping the handler reference.
50
- *
51
- * @example Direction-aware entry animation
52
- * ```tsx
53
- * useRouteEnter(({ route }) => {
54
- * const direction = route.context.browser?.direction;
55
- * ref?.classList.add(
56
- * direction === "back" ? "slide-from-left" : "slide-from-right",
57
- * );
58
- * });
59
- * ```
60
- *
61
- * @example Analytics page-enter event (skip-initial built-in)
62
- * ```tsx
63
- * useRouteEnter(({ route, previousRoute }) => {
64
- * analytics.track("page_enter", {
65
- * route: route.name,
66
- * from: previousRoute.name,
67
- * });
68
- * });
69
- * ```
70
- *
71
- * @example Reading rich transition metadata via `route.transition`
72
- * ```tsx
73
- * useRouteEnter(({ route }) => {
74
- * if (route.transition.redirected) {
75
- * showToast(`Redirected from ${route.transition.from}`);
76
- * }
77
- * if (route.transition.segments.activated.includes("products")) {
78
- * // products subtree just became active
79
- * }
80
- * });
81
- * ```
82
- */
83
- export function useRouteEnter(
84
- handler: RouteEnterHandler,
85
- options?: UseRouteEnterOptions,
86
- ): void {
87
- const routeState = useRoute();
88
- const skipSameRoute = options?.skipSameRoute ?? true;
89
- let lastHandledRoute: State | null = null;
90
-
91
- createEffect(() => {
92
- const { route, previousRoute } = routeState();
93
-
94
- // Early-exit guards, top-down:
95
- //
96
- // - **Skip-initial**: `state.transition.from` is undefined only
97
- // for the very first state committed by `router.start()`.
98
- // - **Skip-same-route**: query-only navigations have
99
- // `transition.from === route.name`. Opt-out via
100
- // `skipSameRoute: false`.
101
- // - **Defensive dedupe + missing `previousRoute`**: same `route`
102
- // ref between effect activations is unexpected on Solid (effects
103
- // run once per dependency change); `!previousRoute` is unreachable
104
- // once `transition.from` is set (the two are populated together by
105
- // core). Both kept for parity with React; v8-ignored.
106
- if (!route.transition.from) {
107
- return;
108
- }
109
- if (skipSameRoute && route.transition.from === route.name) {
110
- return;
111
- }
112
- /* v8 ignore start */
113
- if (lastHandledRoute === route || !previousRoute) {
114
- return;
115
- }
116
- /* v8 ignore stop */
117
-
118
- lastHandledRoute = route;
119
- handler({ route, previousRoute });
120
- });
121
- }