Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PoC] Parent data in child loader #11319

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/data-router/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,17 @@ let router = createBrowserRouter([
],
},
{
id: "deferred",
path: "deferred",
loader: deferredLoader,
Component: DeferredPage,
children: [
{
index: true,
loader: childLoader,
Component: DeferredChild
}
]
},
],
},
Expand Down Expand Up @@ -378,6 +386,8 @@ export function DeferredPage() {
<RenderAwaitedData />
</Await>
</React.Suspense>

<Outlet />
</div>
);
}
Expand All @@ -397,3 +407,21 @@ function RenderAwaitedError() {
</p>
);
}

export async function childLoader({ routeLoaderData }: LoaderFunctionArgs) {
const dataPromise = routeLoaderData("deferred")! as Promise<DeferredRouteLoaderData>
return defer({
lazy2: dataPromise.then(data => data.lazy2.then((message) => "Child: " + message))
})
}

export function DeferredChild(): React.ReactElement {
let data = useLoaderData() as Pick<DeferredRouteLoaderData, 'lazy2'>;
return <div>
<React.Suspense fallback={<p>Child: loading 2...</p>}>
<Await resolve={data.lazy2}>
<RenderAwaitedData />
</Await>
</React.Suspense>
</div>
}
192 changes: 156 additions & 36 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1586,7 +1584,8 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
future.v7_relativeSplatPath
future.v7_relativeSplatPath,
undefined
);

if (request.signal.aborted) {
Expand Down Expand Up @@ -1989,7 +1988,8 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
future.v7_relativeSplatPath
future.v7_relativeSplatPath,
undefined
);

if (fetchRequest.signal.aborted) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2419,29 +2421,127 @@ export function createRouter(init: RouterInit): Router {
}
}

type MatchNode = {
match: AgnosticDataRouteMatch
ancestor?: MatchNode
descendents: MatchNode[]
}

function createMatcherForest(matches: AgnosticDataRouteMatch[]): MatchNode[] {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is possibly overkill but I didn't want to assume that the matches were in any particular order or that matches were strictly limited to a single path (in graph theory terms) even though that probably is the case (although the found router allows for multiple paths which is actually quite nice in some cases so I'd leave that capability in here since there is pretty much zero cost to it).

// Create all the nodes with no edges.
const forest = new Map<string, MatchNode>(matches.map(match => [match.route.id, {match, descendents: []}]))

// Keep track of the routes that we have visited already.
const visited = new Set<string>()

// 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[],
matchesToLoad: AgnosticDataRouteMatch[],
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<DataResult>[] = []
sources.forEach(source => {
const queue = [[source]]
const ancestorData : [string, Promise<any | undefined>][] = []
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(
Expand All @@ -2452,7 +2552,9 @@ export function createRouter(init: RouterInit): Router {
manifest,
mapRouteProperties,
basename,
future.v7_relativeSplatPath
future.v7_relativeSplatPath,
// TODO
{}
);
} else {
let error: ErrorResult = {
Expand Down Expand Up @@ -3148,6 +3250,7 @@ export function createStaticHandler(
mapRouteProperties,
basename,
future.v7_relativeSplatPath,
undefined,
{ isStaticRequest: true, isRouteRequest, requestContext }
);

Expand Down Expand Up @@ -3314,6 +3417,8 @@ export function createStaticHandler(
mapRouteProperties,
basename,
future.v7_relativeSplatPath,
// TODO:
{},
{ isStaticRequest: true, isRouteRequest, requestContext }
)
),
Expand Down Expand Up @@ -3940,15 +4045,19 @@ async function loadLazyRouteModule(
});
}

async function callLoaderOrAction(
type: "loader" | "action",
type LoaderOrActionResultData<T extends "loader" | "action"> =
T extends "loader" ? Record<string, Promise<any | undefined>> : undefined

async function callLoaderOrAction<T extends "loader" | "action">(
type: T,
request: Request,
match: AgnosticDataRouteMatch,
matches: AgnosticDataRouteMatch[],
manifest: RouteManifest,
mapRouteProperties: MapRoutePropertiesFunction,
basename: string,
v7_relativeSplatPath: boolean,
ancestorData: LoaderOrActionResultData<T>,
opts: {
isStaticRequest?: boolean;
isRouteRequest?: boolean;
Expand All @@ -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));
Expand All @@ -3975,8 +4084,19 @@ async function callLoaderOrAction(
]);
};

function loaderToAction(loader: LoaderFunction, ancestorResults: Record<string, Promise<any | undefined>>): 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) {
Expand All @@ -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
Expand Down
10 changes: 9 additions & 1 deletion packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,19 @@ interface DataFunctionArgs<Context> {
// ActionFunction, ActionFunctionArgs, LoaderFunction, LoaderFunctionArgs
// Also, make them a type alias instead of an interface

interface RouteLoaderDataFunction {
(routeId: string):
| Promise<DataFunctionValue>
| undefined;
}

/**
* Arguments passed to loader functions
*/
export interface LoaderFunctionArgs<Context = any>
extends DataFunctionArgs<Context> {}
extends DataFunctionArgs<Context> {
routeLoaderData: RouteLoaderDataFunction
}

/**
* Arguments passed to action functions
Expand Down