diff --git a/examples/data-router/src/app.tsx b/examples/data-router/src/app.tsx
index 4cf6396a7e..1de88ac21b 100644
--- a/examples/data-router/src/app.tsx
+++ b/examples/data-router/src/app.tsx
@@ -49,9 +49,17 @@ let router = createBrowserRouter([
],
},
{
+ id: "deferred",
path: "deferred",
loader: deferredLoader,
Component: DeferredPage,
+ children: [
+ {
+ index: true,
+ loader: childLoader,
+ Component: DeferredChild
+ }
+ ]
},
],
},
@@ -378,6 +386,8 @@ export function DeferredPage() {
+
+
);
}
@@ -397,3 +407,21 @@ function RenderAwaitedError() {
);
}
+
+export async function childLoader({ routeLoaderData }: LoaderFunctionArgs) {
+ const dataPromise = routeLoaderData("deferred")! as Promise
+ return defer({
+ lazy2: dataPromise.then(data => data.lazy2.then((message) => "Child: " + message))
+ })
+}
+
+export function DeferredChild(): React.ReactElement {
+ let data = useLoaderData() as Pick;
+ return
+
Child: loading 2...}>
+
+
+
+
+
+}
diff --git a/packages/router/router.ts b/packages/router/router.ts
index bdea4b0ccf..7fa42b907e 100644
--- a/packages/router/router.ts
+++ b/packages/router/router.ts
@@ -7,47 +7,45 @@ import {
parsePath,
warning,
} from "./history";
-import type {
+import {
ActionFunction,
AgnosticDataRouteMatch,
AgnosticDataRouteObject,
AgnosticRouteObject,
+ convertRouteMatchToUiMatch,
+ convertRoutesToDataRoutes,
DataResult,
DeferredData,
DeferredResult,
DetectErrorBoundaryFunction,
+ ErrorResponseImpl,
ErrorResult,
FormEncType,
FormMethod,
+ getPathContributingMatches,
+ getResolveToMatches,
HTMLFormMethod,
ImmutableRouteKey,
+ immutableRouteKeys,
+ isRouteErrorResponse,
+ joinPaths,
LoaderFunction,
MapRoutePropertiesFunction,
+ matchRoutes,
MutationFormMethod,
RedirectResult,
+ resolveTo,
+ ResultType,
RouteData,
RouteManifest,
ShouldRevalidateFunctionArgs,
+ stripBasename,
Submission,
SuccessResult,
UIMatch,
V7_FormMethod,
V7_MutationFormMethod,
} from "./utils";
-import {
- ErrorResponseImpl,
- ResultType,
- convertRouteMatchToUiMatch,
- convertRoutesToDataRoutes,
- getPathContributingMatches,
- getResolveToMatches,
- immutableRouteKeys,
- isRouteErrorResponse,
- joinPaths,
- matchRoutes,
- resolveTo,
- stripBasename,
-} from "./utils";
////////////////////////////////////////////////////////////////////////////////
//#region Types and Constants
@@ -1586,7 +1584,8 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
- future.v7_relativeSplatPath
+ future.v7_relativeSplatPath,
+ undefined
);
if (request.signal.aborted) {
@@ -1989,7 +1988,8 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
- future.v7_relativeSplatPath
+ future.v7_relativeSplatPath,
+ undefined
);
if (fetchRequest.signal.aborted) {
@@ -2240,7 +2240,9 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
- future.v7_relativeSplatPath
+ future.v7_relativeSplatPath,
+ // TODO
+ {}
);
// Deferred isn't supported for fetcher loads, await everything and treat it
@@ -2419,6 +2421,68 @@ export function createRouter(init: RouterInit): Router {
}
}
+ type MatchNode = {
+ match: AgnosticDataRouteMatch
+ ancestor?: MatchNode
+ descendents: MatchNode[]
+ }
+
+ function createMatcherForest(matches: AgnosticDataRouteMatch[]): MatchNode[] {
+ // Create all the nodes with no edges.
+ const forest = new Map(matches.map(match => [match.route.id, {match, descendents: []}]))
+
+ // Keep track of the routes that we have visited already.
+ const visited = new Set()
+
+ // We iterate over every match doing a breadth first traversal of all its
+ // descendents, creating edges between our nodes as we go. For any two
+ // matches, one is almost certainly forms a subtree of the other so most
+ // iterations will do nothing.
+ matches.forEach(match => {
+ // The queue has the remaining children at the front and the grandchildren
+ // at the back.
+ const queue: {
+ routes: AgnosticDataRouteObject[],
+ ancestor?: MatchNode
+ }[] = [{ routes: [match.route] }]
+ while (queue.length > 0) {
+ const next = queue.shift()
+ if (next) {
+ const { routes, ancestor } = next
+ routes.forEach(route => {
+ if (!visited.has(route.id)) {
+ visited.add(route.id)
+ // We only care about the routes with matches, but we still need
+ // to traverse the others.
+ const matchNode = forest.get(route.id)
+ let nextAncestor = ancestor
+ if (matchNode) {
+ // Add the edge in both directions.
+ matchNode.ancestor = ancestor
+ if (ancestor) {
+ ancestor.descendents.push(matchNode)
+ }
+ // The current node will be the ancestor for its descendents,
+ // otherwise it will be the previous ancestor (we only track
+ // the routes with matches).
+ nextAncestor = matchNode
+ }
+ const nextRoutes = route.children ? route.children : []
+ queue.push({ routes: nextRoutes, ancestor: nextAncestor })
+ }
+ })
+ }
+ }
+ })
+
+ // Now we just need to find all the sources of each tree.
+ const sources: MatchNode[] = []
+ for (const matchNode of forest.values()) {
+ if (!matchNode.ancestor) sources.push(matchNode)
+ }
+ return sources
+ }
+
async function callLoadersAndMaybeResolveData(
currentMatches: AgnosticDataRouteMatch[],
matches: AgnosticDataRouteMatch[],
@@ -2426,22 +2490,58 @@ export function createRouter(init: RouterInit): Router {
fetchersToLoad: RevalidatingFetcher[],
request: Request
) {
+ // We need to ensure that descendents loaders are started after their
+ // ancestors so that we can provide the loader result promise to the
+ // descendents. To do this we create a forest of all routes to load and then
+ // traverse the forest from each source node.
+ const sources = createMatcherForest(matchesToLoad)
+
+ // Now we do depth first traversal of each source, starting each loader,
+ // and keeping track of the ancestor loader results as we go.
+ const allResults: Promise[] = []
+ sources.forEach(source => {
+ const queue = [[source]]
+ const ancestorData : [string, Promise][] = []
+ while (queue.length > 0) {
+ const nodes = queue[0]
+ const node = nodes.shift()
+ if (node) {
+ // Going down.
+ const resultPromise = callLoaderOrAction(
+ "loader",
+ request,
+ node.match,
+ matches,
+ manifest,
+ mapRouteProperties,
+ basename,
+ future.v7_relativeSplatPath,
+ // Need a shallow copy of these since we are mutating them.
+ Object.fromEntries(ancestorData),
+ )
+ allResults.push(resultPromise)
+ const data = resultPromise.then(result => {
+ if (result.type === ResultType.data) {
+ return result.data
+ } else if (isDeferredResult(result)) {
+ return resolveDeferredData(result, request.signal).then(resolved => resolved?.type === ResultType.data ? resolved.data : undefined)
+ }
+ })
+ ancestorData.push([node.match.route.id, data])
+ queue.unshift(node.descendents)
+ } else {
+ // Going back up.
+ ancestorData.shift()
+ queue.shift()
+ }
+ }
+ })
+
// Call all navigation loaders and revalidating fetcher loaders in parallel,
// then slice off the results into separate arrays so we can handle them
// accordingly
let results = await Promise.all([
- ...matchesToLoad.map((match) =>
- callLoaderOrAction(
- "loader",
- request,
- match,
- matches,
- manifest,
- mapRouteProperties,
- basename,
- future.v7_relativeSplatPath
- )
- ),
+ ...allResults.values(),
...fetchersToLoad.map((f) => {
if (f.matches && f.match && f.controller) {
return callLoaderOrAction(
@@ -2452,7 +2552,9 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
- future.v7_relativeSplatPath
+ future.v7_relativeSplatPath,
+ // TODO
+ {}
);
} else {
let error: ErrorResult = {
@@ -3148,6 +3250,7 @@ export function createStaticHandler(
mapRouteProperties,
basename,
future.v7_relativeSplatPath,
+ undefined,
{ isStaticRequest: true, isRouteRequest, requestContext }
);
@@ -3314,6 +3417,8 @@ export function createStaticHandler(
mapRouteProperties,
basename,
future.v7_relativeSplatPath,
+ // TODO:
+ {},
{ isStaticRequest: true, isRouteRequest, requestContext }
)
),
@@ -3940,8 +4045,11 @@ async function loadLazyRouteModule(
});
}
-async function callLoaderOrAction(
- type: "loader" | "action",
+type LoaderOrActionResultData =
+ T extends "loader" ? Record> : undefined
+
+async function callLoaderOrAction(
+ type: T,
request: Request,
match: AgnosticDataRouteMatch,
matches: AgnosticDataRouteMatch[],
@@ -3949,6 +4057,7 @@ async function callLoaderOrAction(
mapRouteProperties: MapRoutePropertiesFunction,
basename: string,
v7_relativeSplatPath: boolean,
+ ancestorData: LoaderOrActionResultData,
opts: {
isStaticRequest?: boolean;
isRouteRequest?: boolean;
@@ -3959,7 +4068,7 @@ async function callLoaderOrAction(
let result;
let onReject: (() => void) | undefined;
- let runHandler = (handler: ActionFunction | LoaderFunction) => {
+ let runHandler = (handler: ActionFunction) => {
// Setup a promise we can race against so that abort signals short circuit
let reject: () => void;
let abortPromise = new Promise((_, r) => (reject = r));
@@ -3975,8 +4084,19 @@ async function callLoaderOrAction(
]);
};
+ function loaderToAction(loader: LoaderFunction, ancestorResults: Record>): ActionFunction {
+ const routeLoaderData = (routeId: string) => ancestorResults[routeId]
+ return (args) => loader({...args, routeLoaderData })
+ }
+
+ function getHandler() {
+ return type === "loader" ?
+ match.route.loader === undefined ? undefined : loaderToAction(match.route.loader, ancestorData!) :
+ match.route.action
+ }
+
try {
- let handler = match.route[type];
+ let handler = getHandler();
if (match.route.lazy) {
if (handler) {
@@ -3999,7 +4119,7 @@ async function callLoaderOrAction(
// Load lazy route module, then run any returned handler
await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
- handler = match.route[type];
+ handler = getHandler();
if (handler) {
// Handler still run even if we got interrupted to maintain consistency
// with un-abortable behavior of handler execution on non-lazy or
diff --git a/packages/router/utils.ts b/packages/router/utils.ts
index 4bb9f48e1d..8a6cc4f019 100644
--- a/packages/router/utils.ts
+++ b/packages/router/utils.ts
@@ -147,11 +147,19 @@ interface DataFunctionArgs {
// ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs
// Also, make them a type alias instead of an interface
+interface RouteLoaderDataFunction {
+ (routeId: string):
+ | Promise
+ | undefined;
+}
+
/**
* Arguments passed to loader functions
*/
export interface LoaderFunctionArgs
- extends DataFunctionArgs {}
+ extends DataFunctionArgs {
+ routeLoaderData: RouteLoaderDataFunction
+}
/**
* Arguments passed to action functions