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