@real-router/solid 0.3.0 → 0.4.0

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 (37) hide show
  1. package/README.md +11 -0
  2. package/dist/cjs/index.js +168 -65
  3. package/dist/esm/index.mjs +169 -66
  4. package/dist/types/RouterProvider.d.ts +1 -0
  5. package/dist/types/RouterProvider.d.ts.map +1 -1
  6. package/dist/types/components/Link.d.ts.map +1 -1
  7. package/dist/types/components/RouteView/components.d.ts.map +1 -1
  8. package/dist/types/components/RouteView/helpers.d.ts +2 -4
  9. package/dist/types/components/RouteView/helpers.d.ts.map +1 -1
  10. package/dist/types/components/RouterErrorBoundary.d.ts.map +1 -1
  11. package/dist/types/createSignalFromSource.d.ts.map +1 -1
  12. package/dist/types/createStoreFromSource.d.ts.map +1 -1
  13. package/dist/types/dom-utils/index.d.ts +1 -1
  14. package/dist/types/dom-utils/index.d.ts.map +1 -1
  15. package/dist/types/dom-utils/link-utils.d.ts +2 -1
  16. package/dist/types/dom-utils/link-utils.d.ts.map +1 -1
  17. package/dist/types/dom-utils/route-announcer.d.ts.map +1 -1
  18. package/dist/types/hooks/sharedNodeSource.d.ts +4 -0
  19. package/dist/types/hooks/sharedNodeSource.d.ts.map +1 -0
  20. package/dist/types/hooks/useRouteNode.d.ts.map +1 -1
  21. package/dist/types/hooks/useRouteNodeStore.d.ts.map +1 -1
  22. package/dist/types/hooks/useRouteStore.d.ts.map +1 -1
  23. package/dist/types/hooks/useRouterTransition.d.ts.map +1 -1
  24. package/package.json +7 -7
  25. package/src/RouterProvider.tsx +1 -1
  26. package/src/components/Link.tsx +51 -8
  27. package/src/components/RouteView/RouteView.tsx +13 -13
  28. package/src/components/RouteView/components.tsx +12 -6
  29. package/src/components/RouteView/helpers.tsx +19 -17
  30. package/src/components/RouterErrorBoundary.tsx +4 -6
  31. package/src/createSignalFromSource.ts +9 -1
  32. package/src/createStoreFromSource.ts +1 -3
  33. package/src/hooks/sharedNodeSource.ts +30 -0
  34. package/src/hooks/useRouteNode.tsx +2 -4
  35. package/src/hooks/useRouteNodeStore.tsx +2 -3
  36. package/src/hooks/useRouteStore.tsx +0 -8
  37. package/src/hooks/useRouterTransition.tsx +15 -3
@@ -9,12 +9,48 @@ import {
9
9
  buildHref,
10
10
  buildActiveClassName,
11
11
  } from "../dom-utils/index.js";
12
- import { useRouter } from "../hooks/useRouter";
13
12
 
14
13
  import type { LinkProps } from "../types";
15
- import type { Params } from "@real-router/core";
14
+ import type { Params, Router } from "@real-router/core";
15
+ import type { RouterSource } from "@real-router/sources";
16
16
  import type { JSX } from "solid-js";
17
17
 
18
+ // Slow-path source cache: shared per-router, keyed by routeName + params + flags.
19
+ // Captured slow-path values are stable per Link (props captured at init), so the
20
+ // cache key is guaranteed stable for the lifetime of any consumer.
21
+ const activeSourceCache = new WeakMap<
22
+ Router,
23
+ Map<string, RouterSource<boolean>>
24
+ >();
25
+
26
+ function getOrCreateActiveSource(
27
+ router: Router,
28
+ routeName: string,
29
+ routeParams: Params,
30
+ activeStrict: boolean,
31
+ ignoreQueryParams: boolean,
32
+ ): RouterSource<boolean> {
33
+ let perRouter = activeSourceCache.get(router);
34
+
35
+ if (!perRouter) {
36
+ perRouter = new Map();
37
+ activeSourceCache.set(router, perRouter);
38
+ }
39
+
40
+ const key = `${routeName}|${JSON.stringify(routeParams)}|${activeStrict}|${ignoreQueryParams}`;
41
+ let source = perRouter.get(key);
42
+
43
+ if (!source) {
44
+ source = createActiveRouteSource(router, routeName, routeParams, {
45
+ strict: activeStrict,
46
+ ignoreQueryParams,
47
+ });
48
+ perRouter.set(key, source);
49
+ }
50
+
51
+ return source;
52
+ }
53
+
18
54
  export function Link<P extends Params = Params>(
19
55
  props: Readonly<LinkProps<P>>,
20
56
  ): JSX.Element {
@@ -42,11 +78,15 @@ export function Link<P extends Params = Params>(
42
78
  "children",
43
79
  ]);
44
80
 
45
- const router = useRouter();
46
81
  const ctx = useContext(RouterContext);
47
82
 
83
+ if (!ctx) {
84
+ throw new Error("Link must be used within a RouterProvider");
85
+ }
86
+
87
+ const router = ctx.router;
88
+
48
89
  const useFastPath =
49
- ctx?.routeSelector &&
50
90
  !local.activeStrict &&
51
91
  local.ignoreQueryParams &&
52
92
  local.routeParams === EMPTY_PARAMS;
@@ -54,10 +94,13 @@ export function Link<P extends Params = Params>(
54
94
  const isActive = useFastPath
55
95
  ? () => ctx.routeSelector(local.routeName)
56
96
  : createSignalFromSource(
57
- createActiveRouteSource(router, local.routeName, local.routeParams, {
58
- strict: local.activeStrict,
59
- ignoreQueryParams: local.ignoreQueryParams,
60
- }),
97
+ getOrCreateActiveSource(
98
+ router,
99
+ local.routeName,
100
+ local.routeParams,
101
+ local.activeStrict,
102
+ local.ignoreQueryParams,
103
+ ),
61
104
  );
62
105
 
63
106
  const href = createMemo(() =>
@@ -1,4 +1,4 @@
1
- import { children as resolveChildren } from "solid-js";
1
+ import { children as resolveChildren, createMemo } from "solid-js";
2
2
 
3
3
  import { Match, NotFound } from "./components";
4
4
  import { buildRenderList, collectElements } from "./helpers";
@@ -13,6 +13,14 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
13
13
 
14
14
  const resolved = resolveChildren(() => props.children);
15
15
 
16
+ const elements = createMemo(() => {
17
+ const arr: RouteViewMarker[] = [];
18
+
19
+ collectElements(resolved(), arr);
20
+
21
+ return arr;
22
+ });
23
+
16
24
  return (
17
25
  <>
18
26
  {(() => {
@@ -22,24 +30,16 @@ function RouteViewRoot(props: Readonly<RouteViewProps>): JSX.Element {
22
30
  return null;
23
31
  }
24
32
 
25
- const elements: RouteViewMarker[] = [];
26
-
27
- collectElements(resolved(), elements);
28
-
29
- const { rendered } = buildRenderList(
30
- elements,
33
+ const rendered = buildRenderList(
34
+ elements(),
31
35
  state.route.name,
32
36
  props.nodeName,
33
37
  );
34
38
 
35
- if (rendered.length > 0) {
36
- return rendered;
37
- }
38
-
39
- return null;
39
+ return rendered.length > 0 ? rendered : null;
40
40
  })()}
41
41
  </>
42
- ) as unknown as JSX.Element;
42
+ );
43
43
  }
44
44
 
45
45
  RouteViewRoot.displayName = "RouteView";
@@ -1,9 +1,11 @@
1
1
  import type { MatchProps, NotFoundProps } from "./types";
2
2
  import type { JSX } from "solid-js";
3
3
 
4
- export const MATCH_MARKER = Symbol.for("RouteView.Match");
4
+ // Local (non-global) Symbols Symbol.for() would expose markers to spoofing
5
+ // via the global Symbol registry. See Gotchas section "RouteView Marker Objects".
6
+ export const MATCH_MARKER = Symbol("RouteView.Match");
5
7
 
6
- export const NOT_FOUND_MARKER = Symbol.for("RouteView.NotFound");
8
+ export const NOT_FOUND_MARKER = Symbol("RouteView.NotFound");
7
9
 
8
10
  export interface MatchMarker {
9
11
  $$type: typeof MATCH_MARKER;
@@ -21,7 +23,7 @@ export interface NotFoundMarker {
21
23
  export type RouteViewMarker = MatchMarker | NotFoundMarker;
22
24
 
23
25
  export function Match(props: MatchProps): JSX.Element {
24
- const result = {
26
+ const result: MatchMarker = {
25
27
  $$type: MATCH_MARKER,
26
28
  segment: props.segment,
27
29
  exact: props.exact ?? false,
@@ -29,21 +31,25 @@ export function Match(props: MatchProps): JSX.Element {
29
31
  get children(): JSX.Element {
30
32
  return props.children;
31
33
  },
32
- } as MatchMarker;
34
+ };
33
35
 
36
+ // Marker object is identified by $$type Symbol in RouteView/helpers.tsx,
37
+ // not rendered as JSX. Cast required because JSX.Element does not include
38
+ // arbitrary marker shapes.
34
39
  return result as unknown as JSX.Element;
35
40
  }
36
41
 
37
42
  Match.displayName = "RouteView.Match";
38
43
 
39
44
  export function NotFound(props: NotFoundProps): JSX.Element {
40
- const result = {
45
+ const result: NotFoundMarker = {
41
46
  $$type: NOT_FOUND_MARKER,
42
47
  get children(): JSX.Element {
43
48
  return props.children;
44
49
  },
45
- } as NotFoundMarker;
50
+ };
46
51
 
52
+ // See Match for the marker-pattern rationale.
47
53
  return result as unknown as JSX.Element;
48
54
  }
49
55
 
@@ -11,7 +11,7 @@ import type {
11
11
  } from "./components";
12
12
  import type { JSX } from "solid-js";
13
13
 
14
- function isSegmentMatch(
14
+ export function isSegmentMatch(
15
15
  routeName: string,
16
16
  fullSegmentName: string,
17
17
  exact: boolean,
@@ -66,7 +66,7 @@ export function buildRenderList(
66
66
  elements: RouteViewMarker[],
67
67
  routeName: string,
68
68
  nodeName: string,
69
- ): { rendered: JSX.Element[]; activeMatchFound: boolean } {
69
+ ): JSX.Element[] {
70
70
  let notFoundChildren: JSX.Element | null = null;
71
71
  let activeMatchFound = false;
72
72
  const rendered: JSX.Element[] = [];
@@ -77,23 +77,25 @@ export function buildRenderList(
77
77
  continue;
78
78
  }
79
79
 
80
+ if (activeMatchFound) {
81
+ continue;
82
+ }
83
+
80
84
  const { segment, exact, fallback } = child;
81
85
  const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
82
- const isActive =
83
- !activeMatchFound && isSegmentMatch(routeName, fullSegmentName, exact);
84
-
85
- if (isActive) {
86
- activeMatchFound = true;
87
- const matchContent = child.children;
88
- const content =
89
- fallback === undefined ? (
90
- matchContent
91
- ) : (
92
- <Suspense fallback={fallback}>{matchContent}</Suspense>
93
- );
94
-
95
- rendered.push(content);
86
+
87
+ if (!isSegmentMatch(routeName, fullSegmentName, exact)) {
88
+ continue;
96
89
  }
90
+
91
+ activeMatchFound = true;
92
+ rendered.push(
93
+ fallback === undefined ? (
94
+ child.children
95
+ ) : (
96
+ <Suspense fallback={fallback}>{child.children}</Suspense>
97
+ ),
98
+ );
97
99
  }
98
100
 
99
101
  if (
@@ -104,5 +106,5 @@ export function buildRenderList(
104
106
  rendered.push(notFoundChildren);
105
107
  }
106
108
 
107
- return { rendered, activeMatchFound };
109
+ return rendered;
108
110
  }
@@ -1,4 +1,4 @@
1
- import { createEffect, createMemo, createSignal } from "solid-js";
1
+ import { createEffect, createMemo, createSignal, Show } from "solid-js";
2
2
 
3
3
  import { useRouterError } from "../hooks/useRouterError";
4
4
 
@@ -43,11 +43,9 @@ export function RouterErrorBoundary(
43
43
  return (
44
44
  <>
45
45
  {props.children}
46
- {(() => {
47
- const error = visibleError();
48
-
49
- return error ? props.fallback(error, resetError) : null;
50
- })()}
46
+ <Show when={visibleError()}>
47
+ {(error) => props.fallback(error(), resetError)}
48
+ </Show>
51
49
  </>
52
50
  );
53
51
  }
@@ -8,10 +8,18 @@ export function createSignalFromSource<T>(
8
8
  ): Accessor<T> {
9
9
  const [value, setValue] = createSignal<T>(source.getSnapshot());
10
10
 
11
+ const sync = (): T => source.getSnapshot();
12
+
11
13
  const unsubscribe = source.subscribe(() => {
12
- setValue(() => source.getSnapshot());
14
+ setValue(sync);
13
15
  });
14
16
 
17
+ // Re-read after subscribe: lazy sources reconcile their snapshot in
18
+ // onFirstSubscribe (when reused after disconnect via cache). Listener is not
19
+ // notified for that internal update, so we must sync the signal manually.
20
+ // No-op when snapshot is unchanged (signal equality check).
21
+ setValue(sync);
22
+
15
23
  onCleanup(() => {
16
24
  unsubscribe();
17
25
  });
@@ -6,9 +6,7 @@ import type { RouterSource } from "@real-router/sources";
6
6
  export function createStoreFromSource<T extends object>(
7
7
  source: RouterSource<T>,
8
8
  ): T {
9
- const [state, setState] = createStore<T>(
10
- structuredClone(source.getSnapshot()),
11
- );
9
+ const [state, setState] = createStore<T>({ ...source.getSnapshot() });
12
10
 
13
11
  const unsubscribe = source.subscribe(() => {
14
12
  setState(reconcile(source.getSnapshot()));
@@ -0,0 +1,30 @@
1
+ import { createRouteNodeSource } from "@real-router/sources";
2
+
3
+ import type { Router } from "@real-router/core";
4
+ import type { RouteNodeSnapshot, RouterSource } from "@real-router/sources";
5
+
6
+ const cache = new WeakMap<
7
+ Router,
8
+ Map<string, RouterSource<RouteNodeSnapshot>>
9
+ >();
10
+
11
+ export function getOrCreateNodeSource(
12
+ router: Router,
13
+ nodeName: string,
14
+ ): RouterSource<RouteNodeSnapshot> {
15
+ let perRouter = cache.get(router);
16
+
17
+ if (!perRouter) {
18
+ perRouter = new Map();
19
+ cache.set(router, perRouter);
20
+ }
21
+
22
+ let source = perRouter.get(nodeName);
23
+
24
+ if (!source) {
25
+ source = createRouteNodeSource(router, nodeName);
26
+ perRouter.set(nodeName, source);
27
+ }
28
+
29
+ return source;
30
+ }
@@ -1,6 +1,5 @@
1
- import { createRouteNodeSource } from "@real-router/sources";
2
-
3
1
  import { createSignalFromSource } from "../createSignalFromSource";
2
+ import { getOrCreateNodeSource } from "./sharedNodeSource";
4
3
  import { useRouter } from "./useRouter";
5
4
 
6
5
  import type { RouteState } from "../types";
@@ -8,7 +7,6 @@ import type { Accessor } from "solid-js";
8
7
 
9
8
  export function useRouteNode(nodeName: string): Accessor<RouteState> {
10
9
  const router = useRouter();
11
- const store = createRouteNodeSource(router, nodeName);
12
10
 
13
- return createSignalFromSource(store);
11
+ return createSignalFromSource(getOrCreateNodeSource(router, nodeName));
14
12
  }
@@ -1,6 +1,5 @@
1
- import { createRouteNodeSource } from "@real-router/sources";
2
-
3
1
  import { createStoreFromSource } from "../createStoreFromSource";
2
+ import { getOrCreateNodeSource } from "./sharedNodeSource";
4
3
  import { useRouter } from "./useRouter";
5
4
 
6
5
  import type { RouteState } from "../types";
@@ -8,5 +7,5 @@ import type { RouteState } from "../types";
8
7
  export function useRouteNodeStore(nodeName: string): RouteState {
9
8
  const router = useRouter();
10
9
 
11
- return createStoreFromSource(createRouteNodeSource(router, nodeName));
10
+ return createStoreFromSource(getOrCreateNodeSource(router, nodeName));
12
11
  }
@@ -1,19 +1,11 @@
1
1
  import { createRouteSource } from "@real-router/sources";
2
- import { useContext } from "solid-js";
3
2
 
4
- import { RouteContext } from "../context";
5
3
  import { createStoreFromSource } from "../createStoreFromSource";
6
4
  import { useRouter } from "./useRouter";
7
5
 
8
6
  import type { RouteState } from "../types";
9
7
 
10
8
  export function useRouteStore(): RouteState {
11
- const ctx = useContext(RouteContext);
12
-
13
- if (!ctx) {
14
- throw new Error("useRouteStore must be used within a RouterProvider");
15
- }
16
-
17
9
  const router = useRouter();
18
10
 
19
11
  return createStoreFromSource(createRouteSource(router));
@@ -3,12 +3,24 @@ import { createTransitionSource } from "@real-router/sources";
3
3
  import { createSignalFromSource } from "../createSignalFromSource";
4
4
  import { useRouter } from "./useRouter";
5
5
 
6
- import type { RouterTransitionSnapshot } from "@real-router/sources";
6
+ import type { Router } from "@real-router/core";
7
+ import type {
8
+ RouterSource,
9
+ RouterTransitionSnapshot,
10
+ } from "@real-router/sources";
7
11
  import type { Accessor } from "solid-js";
8
12
 
13
+ const cache = new WeakMap<Router, RouterSource<RouterTransitionSnapshot>>();
14
+
9
15
  export function useRouterTransition(): Accessor<RouterTransitionSnapshot> {
10
16
  const router = useRouter();
11
- const store = createTransitionSource(router);
12
17
 
13
- return createSignalFromSource(store);
18
+ let source = cache.get(router);
19
+
20
+ if (!source) {
21
+ source = createTransitionSource(router);
22
+ cache.set(router, source);
23
+ }
24
+
25
+ return createSignalFromSource(source);
14
26
  }