@rangojs/router 0.0.0-experimental.63 → 0.0.0-experimental.64

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.
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
98
98
  * the isRscRequest decision in rsc-rendering.ts.
99
99
  *
100
100
  * Note: response/mime routes are excluded by the caller — this function
101
- * runs after previewMatch() classifies the route type.
101
+ * runs after classifyRequest() determines the request mode.
102
102
  */
103
103
  export function mayNeedSSR(request: Request, url: URL): boolean {
104
104
  if (
@@ -290,6 +290,12 @@ export interface RequestContext<
290
290
 
291
291
  /** @internal Router basename for this request (used by redirect()) */
292
292
  _basename?: string;
293
+
294
+ /**
295
+ * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
296
+ * to avoid a second resolveRoute call. Cleared on HMR invalidation.
297
+ */
298
+ _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
293
299
  }
294
300
 
295
301
  /**
@@ -322,6 +328,7 @@ export type PublicRequestContext<
322
328
  | "_basename"
323
329
  | "_setStatus"
324
330
  | "_variables"
331
+ | "_classifiedRoute"
325
332
  | "res"
326
333
  >;
327
334
 
@@ -284,7 +284,10 @@ export type PathHelpers<TEnv> = {
284
284
  /**
285
285
  * Attach a loading component to the current route/layout
286
286
  */
287
- loading: (component: ReactNode, options?: { ssr?: boolean }) => LoadingItem;
287
+ loading: (
288
+ component: ReactNode | (() => ReactNode),
289
+ options?: { ssr?: boolean },
290
+ ) => LoadingItem;
288
291
 
289
292
  /**
290
293
  * Attach an error boundary to catch errors in this segment
@@ -1,9 +1,70 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useContext, useEffect, useRef, useState } from "react";
3
+ import {
4
+ isValidElement,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ type ReactNode,
12
+ } from "react";
4
13
  import { OutletContext, type OutletContextValue } from "./outlet-context.js";
5
14
  import type { LoaderDefinition, LoadOptions } from "./types.js";
6
15
 
16
+ /**
17
+ * Extract a specific loader's data from a content ReactNode.
18
+ *
19
+ * When a route registers loaders via loader(), the resolved data lives in
20
+ * the route's OutletProvider (rendered as <Outlet /> content). Parallel
21
+ * slots are siblings of <Outlet />, so they can't find it by walking
22
+ * the parent context chain. This helper traverses wrapper elements
23
+ * (MountContextProvider, ViewTransition, etc.) to reach the OutletProvider
24
+ * and extract the loader data directly.
25
+ */
26
+ const NOT_FOUND = Symbol("not-found");
27
+
28
+ function extractContentLoaderData(
29
+ node: ReactNode,
30
+ loaderId: string,
31
+ ): unknown | typeof NOT_FOUND {
32
+ if (!isValidElement(node)) return NOT_FOUND;
33
+ const props = node.props as Record<string, any> | undefined;
34
+ if (!props) return NOT_FOUND;
35
+
36
+ // Direct OutletProvider with loaderData
37
+ if (props.loaderData && loaderId in props.loaderData) {
38
+ return props.loaderData[loaderId];
39
+ }
40
+
41
+ // LoaderBoundary: loaderIds + loaderDataPromise (already resolved array).
42
+ // When the segment has loading(), loaderData is resolved inside
43
+ // LoaderBoundary via use(). If the promise was pre-awaited (forceAwait
44
+ // or isAction), the prop is a raw array we can index into.
45
+ if (
46
+ props.loaderIds &&
47
+ Array.isArray(props.loaderIds) &&
48
+ props.loaderDataPromise &&
49
+ !(props.loaderDataPromise instanceof Promise)
50
+ ) {
51
+ const idx = (props.loaderIds as string[]).indexOf(loaderId);
52
+ if (idx !== -1) {
53
+ const data = (props.loaderDataPromise as any[])[idx];
54
+ // loaderDataPromise entries may be { ok, data } result objects
55
+ if (data && typeof data === "object" && "ok" in data) {
56
+ return data.ok ? data.data : NOT_FOUND;
57
+ }
58
+ return data;
59
+ }
60
+ }
61
+
62
+ // Traverse into wrapper elements (MountContextProvider, ViewTransition,
63
+ // Suspense wrappers, etc.)
64
+ if (props.children) return extractContentLoaderData(props.children, loaderId);
65
+ return NOT_FOUND;
66
+ }
67
+
7
68
  /**
8
69
  * Payload returned by loader RSC requests
9
70
  */
@@ -71,19 +132,27 @@ function useLoaderInternal<T>(
71
132
  const context = useContext(OutletContext);
72
133
 
73
134
  // Get data from context (SSR/navigation)
74
- const getContextData = useCallback((): T | undefined => {
135
+ const contextData = useMemo((): T | undefined => {
75
136
  let current: OutletContextValue | null | undefined = context;
76
137
  while (current) {
77
138
  if (current.loaderData && loader.$$id in current.loaderData) {
78
139
  return current.loaderData[loader.$$id] as T;
79
140
  }
141
+ // Check content element — the route's OutletProvider is rendered as
142
+ // <Outlet /> content (a child), so its loaderData isn't in the parent
143
+ // chain. Parallel slots need to reach into it to find route-level loaders.
144
+ const contentData = extractContentLoaderData(
145
+ current.content,
146
+ loader.$$id,
147
+ );
148
+ if (contentData !== NOT_FOUND) {
149
+ return contentData as T;
150
+ }
80
151
  current = current.parent;
81
152
  }
82
153
  return undefined;
83
154
  }, [context, loader.$$id]);
84
155
 
85
- const contextData = getContextData();
86
-
87
156
  // Local state for fetched data (from load() calls)
88
157
  const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
89
158
  const [isLoading, setIsLoading] = useState(false);