Skip to content

Commit 65920e8

Browse files
committed
Refactor dataStrategy for easier RSC abstraction
1 parent b8cf1b6 commit 65920e8

File tree

2 files changed

+85
-98
lines changed

2 files changed

+85
-98
lines changed

integration/single-fetch-test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { test, expect } from "@playwright/test";
22
import {
33
UNSAFE_ServerMode as ServerMode,
44
UNSAFE_SingleFetchRedirectSymbol as SingleFetchRedirectSymbol,
5+
UNSAFE_ServerMode,
56
} from "react-router";
67

78
import {

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 84 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -316,145 +316,130 @@ async function singleFetchLoaderNavigationStrategy(
316316
matches: DataStrategyFunctionArgs["matches"],
317317
basename: string | undefined
318318
) {
319-
// Track which routes need a server load - in case we need to tack on a
320-
// `_routes` param
321-
let routesParams = new Set<string>();
319+
// Routes that need to be loaded from the server
320+
let serverLoaderRouteDfds = new Map<string, Deferred>();
322321

323-
// We only add `_routes` when one or more routes opts out of a load via
324-
// `shouldRevalidate` or `clientLoader`
322+
// Did any routes opt-out of a server loader via`shouldRevalidate`/`clientLoader`?
325323
let foundOptOutRoute = false;
326324

327-
// Deferreds for each route so we can be sure they've all loaded via
328-
// `match.resolve()`, and a singular promise that can tell us all routes
329-
// have been resolved
325+
// Deferreds to ensure all routes have loaded via match.resolve()
330326
let routeDfds = matches.map(() => createDeferred<void>());
331-
let routesLoadedPromise = Promise.all(routeDfds.map((d) => d.promise));
332327

333-
// Deferred that we'll use for the call to the server that each match can
334-
// await and parse out it's specific result
335-
let singleFetchDfd = createDeferred<SingleFetchResults>();
328+
// We'll build up this results object as we loop through matches
329+
let results: Record<string, DataStrategyResult> = {};
336330

337-
// Base URL and RequestInit for calls to the server
331+
// Base URL/RequestInit for calls to the server
338332
let url = stripIndexParam(singleFetchUrl(request.url, basename));
339333
let init = await createRequestInit(request);
340334

341-
// We'll build up this results object as we loop through matches
342-
let results: Record<string, DataStrategyResult> = {};
343-
344335
let resolvePromise = Promise.all(
345336
matches.map(async (m, i) =>
346337
m.resolve(async (handler) => {
347338
routeDfds[i].resolve();
348339

349-
let manifestRoute = manifest.routes[m.route.id];
350-
351-
// Note: If this logic changes for routes that should not participate
352-
// in Single Fetch, make sure you update getLowestLoadingIndex above
353-
// as well
354-
if (!m.shouldLoad) {
355-
// If we're not yet initialized and this is the initial load, respect
356-
// `shouldLoad` because we're only dealing with `clientLoader.hydrate`
357-
// routes which will fall into the `clientLoader` section below.
358-
if (!router.state.initialized) {
359-
return;
360-
}
340+
let { id: routeId, shouldRevalidate } = m.route;
341+
let manifestRoute = manifest.routes[routeId];
342+
invariant(manifestRoute, "No manifest route found for dataStrategy");
343+
let { hasLoader, hasClientLoader } = manifestRoute;
344+
345+
// Short circuit if there's no loader to call. Give this route an
346+
// `undefined` result because we always have a `loader` on the client
347+
// route in framework mode (for loading module/styles) so this ensures
348+
// it doesn't think it's waiting for data and trigger `HydrateFallback`
349+
if (!hasLoader && !hasClientLoader && !window.__reactRouterHdrActive) {
350+
results[routeId] = { type: "data", result: undefined };
351+
return;
352+
}
361353

362-
// Otherwise, we opt out if we currently have data and a
363-
// `shouldRevalidate` function. This implies that the user opted out
364-
// via `shouldRevalidate`
365-
if (
366-
m.route.id in router.state.loaderData &&
367-
manifestRoute &&
368-
m.route.shouldRevalidate
369-
) {
370-
if (manifestRoute.hasLoader) {
371-
// If we have a server loader, make sure we don't include it in the
372-
// single fetch .data request
373-
foundOptOutRoute = true;
374-
}
375-
return;
354+
// Respect `shouldLoad` on initial load because we're only dealing with
355+
// `clientLoader.hydrate` routes which will fall into the `clientLoader`
356+
// section below.
357+
if (!m.shouldLoad && !router.state.initialized) {
358+
return;
359+
}
360+
361+
// Otherwise, skip the call if we currently have data and a `shouldRevalidate`
362+
// function - which implies that the user opted out via `shouldRevalidate`
363+
if (
364+
!m.shouldLoad &&
365+
routeId in router.state.loaderData &&
366+
shouldRevalidate
367+
) {
368+
// If a server loader exists, ensure it's excluded from the .data request
369+
if (hasLoader) {
370+
foundOptOutRoute = true;
376371
}
372+
return;
377373
}
378374

379375
// When a route has a client loader, it opts out of the singular call and
380376
// calls it's server loader via `serverLoader()` using a `?_routes` param
381-
if (manifestRoute && manifestRoute.hasClientLoader) {
382-
if (manifestRoute.hasLoader) {
377+
if (hasClientLoader) {
378+
if (hasLoader) {
383379
foundOptOutRoute = true;
384380
}
385381
try {
386-
let result = await fetchSingleLoader(
387-
handler,
388-
url,
389-
init,
390-
m.route.id
391-
);
392-
results[m.route.id] = { type: "data", result };
382+
let result = await fetchSingleLoader(handler, url, init, routeId);
383+
results[routeId] = { type: "data", result };
393384
} catch (e) {
394-
results[m.route.id] = { type: "error", result: e };
385+
results[routeId] = { type: "error", result: e };
395386
}
396387
return;
397388
}
398389

399-
// Load this route on the server if it has a loader
400-
if (manifestRoute && manifestRoute.hasLoader) {
401-
routesParams.add(m.route.id);
402-
}
403-
404-
// Lump this match in with the others on a singular promise
405-
try {
406-
let result = await handler(async () => {
407-
let data = await singleFetchDfd.promise;
408-
return unwrapSingleFetchResults(data, m.route.id);
409-
});
410-
results[m.route.id] = {
411-
type: "data",
412-
result,
413-
};
414-
} catch (e) {
415-
results[m.route.id] = {
416-
type: "error",
417-
result: e,
418-
};
419-
}
390+
let dfd = createDeferred();
391+
serverLoaderRouteDfds.set(routeId, dfd);
392+
await handler(() => dfd.promise);
420393
})
421394
)
422395
);
423396

424397
// Wait for all routes to resolve above before we make the HTTP call
425-
await routesLoadedPromise;
398+
await Promise.all(routeDfds.map((d) => d.promise));
426399

427-
// We can skip the server call:
428-
// - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
429-
// - If there are no routes to fetch from the server
400+
// Only make the server call if:
401+
// - We have loaders on the server that need fetching
402+
// - We're already hydrated - during initial hydration only clientLoaders can
403+
// run (via `clientLoader.hydrate`) which are handled above
430404
//
431405
// One exception - if we are performing an HDR revalidation we have to call
432406
// the server in case a new loader has shown up that the manifest doesn't yet
433407
// know about
434408
if (
435-
(!router.state.initialized || routesParams.size === 0) &&
436-
!window.__reactRouterHdrActive
409+
(serverLoaderRouteDfds.size > 0 && router.state.initialized) ||
410+
window.__reactRouterHdrActive
437411
) {
438-
singleFetchDfd.resolve({});
439-
} else {
412+
// When routes have opted out, add a `_routes` param to filter server loaders
413+
// Skipped in `ssr:false` because we expect to be loading static `.data` files
414+
if (ssr && foundOptOutRoute && serverLoaderRouteDfds.size > 0) {
415+
let routes = [...serverLoaderRouteDfds.keys()].join(",");
416+
url.searchParams.set("_routes", routes);
417+
}
418+
440419
try {
441-
// When one or more routes have opted out, we add a _routes param to
442-
// limit the loaders to those that have a server loader and did not
443-
// opt out
444-
if (ssr && foundOptOutRoute && routesParams.size > 0) {
445-
url.searchParams.set(
446-
"_routes",
447-
matches
448-
.filter((m) => routesParams.has(m.route.id))
449-
.map((m) => m.route.id)
450-
.join(",")
451-
);
420+
let { data } = (await fetchAndDecode(url, init)) as {
421+
data: SingleFetchResults;
422+
};
423+
for (let [routeId, dfd] of serverLoaderRouteDfds) {
424+
try {
425+
results[routeId] = {
426+
type: "data",
427+
result: unwrapSingleFetchResults(data, routeId),
428+
};
429+
dfd.resolve(results[routeId]);
430+
} catch (e) {
431+
results[routeId] = { type: "error", result: e };
432+
dfd.reject(results[routeId]);
433+
}
452434
}
453-
454-
let data = await fetchAndDecode(url, init);
455-
singleFetchDfd.resolve(data.data as SingleFetchResults);
456435
} catch (e) {
457-
singleFetchDfd.reject(e as Error);
436+
serverLoaderRouteDfds.forEach((dfd, routeId) => {
437+
results[routeId] = {
438+
type: "error",
439+
result: e,
440+
};
441+
dfd.reject(results[routeId]);
442+
});
458443
}
459444
}
460445

@@ -673,17 +658,18 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
673658
}
674659
}
675660

661+
type Deferred = ReturnType<typeof createDeferred>;
676662
function createDeferred<T = unknown>() {
677663
let resolve: (val?: any) => Promise<void>;
678-
let reject: (error?: Error) => Promise<void>;
664+
let reject: (error?: unknown) => Promise<void>;
679665
let promise = new Promise<T>((res, rej) => {
680666
resolve = async (val: T) => {
681667
res(val);
682668
try {
683669
await promise;
684670
} catch (e) {}
685671
};
686-
reject = async (error?: Error) => {
672+
reject = async (error?: unknown) => {
687673
rej(error);
688674
try {
689675
await promise;

0 commit comments

Comments
 (0)