Skip to content

Commit a2d5768

Browse files
Fix RSC client loader support (#13663)
1 parent 25aeaad commit a2d5768

File tree

2 files changed

+46
-17
lines changed

2 files changed

+46
-17
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,13 @@ type GetRouteInfoFunction = (match: DataRouteMatch) => {
166166
hasShouldRevalidate: boolean;
167167
};
168168

169+
type ShouldAllowOptOutFunction = (match: DataRouteMatch) => boolean;
170+
169171
export type FetchAndDecodeFunction = (
170172
args: DataStrategyFunctionArgs,
171173
basename: string | undefined,
172-
targetRoutes?: string[]
174+
targetRoutes?: string[],
175+
shouldAllowOptOut?: ShouldAllowOptOutFunction
173176
) => Promise<{ status: number; data: DecodedSingleFetchResults }>;
174177

175178
export function getTurboStreamSingleFetchDataStrategy(
@@ -203,7 +206,8 @@ export function getSingleFetchDataStrategyImpl(
203206
getRouteInfo: GetRouteInfoFunction,
204207
fetchAndDecode: FetchAndDecodeFunction,
205208
ssr: boolean,
206-
basename: string | undefined
209+
basename: string | undefined,
210+
shouldAllowOptOut: ShouldAllowOptOutFunction = () => true
207211
): DataStrategyFunction {
208212
return async (args) => {
209213
let { request, matches, fetcherKey } = args;
@@ -266,7 +270,8 @@ export function getSingleFetchDataStrategyImpl(
266270
getRouteInfo,
267271
fetchAndDecode,
268272
ssr,
269-
basename
273+
basename,
274+
shouldAllowOptOut
270275
);
271276
};
272277
}
@@ -354,7 +359,8 @@ async function singleFetchLoaderNavigationStrategy(
354359
getRouteInfo: GetRouteInfoFunction,
355360
fetchAndDecode: FetchAndDecodeFunction,
356361
ssr: boolean,
357-
basename: string | undefined
362+
basename: string | undefined,
363+
shouldAllowOptOut: (match: DataRouteMatch) => boolean = () => true
358364
) {
359365
// Track which routes need a server load for use in a `_routes` param
360366
let routesParams = new Set<string>();
@@ -396,7 +402,7 @@ async function singleFetchLoaderNavigationStrategy(
396402

397403
// When a route has a client loader, it opts out of the singular call and
398404
// calls it's server loader via `serverLoader()` using a `?_routes` param
399-
if (hasClientLoader) {
405+
if (shouldAllowOptOut(m) && hasClientLoader) {
400406
if (hasLoader) {
401407
foundOptOutRoute = true;
402408
}

packages/react-router/lib/rsc/browser.tsx

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,13 @@ export function getRSCSingleFetchDataStrategy(
271271
// pass map into fetchAndDecode so it can add payloads
272272
getFetchAndDecodeViaRSC(decode),
273273
ssr,
274-
basename
274+
basename,
275+
// If we don't have an element, we need to hit the server loader flow
276+
// regardless of whether the client loader calls `serverLoader` or not,
277+
// otherwise we'll have nothing to render.
278+
// TODO: Do we need to account for API routes? Do we need a
279+
// `match.hasComponent` flag?
280+
(match) => match.route.element != null
275281
);
276282
return async (args) =>
277283
args.unstable_runClientMiddleware(async () => {
@@ -288,17 +294,27 @@ export function getRSCSingleFetchDataStrategy(
288294
context.set(renderedRoutesContext, []);
289295
let results = await dataStrategy(args);
290296
// patch into router from all payloads in map
291-
const renderedRouteById = new Map(
292-
context.get(renderedRoutesContext).map((r) => [r.id, r])
293-
);
297+
// TODO: Confirm that it's correct for us to have multiple rendered routes
298+
// with the same ID. This is currently happening in `clientLoader` cases
299+
// where we're calling `fetchAndDecode` multiple times. This may be a
300+
// sign of a logical error in how we're handling client loader routes.
301+
const renderedRoutesById = new Map<string, RenderedRoute[]>();
302+
for (const route of context.get(renderedRoutesContext)) {
303+
if (!renderedRoutesById.has(route.id)) {
304+
renderedRoutesById.set(route.id, []);
305+
}
306+
renderedRoutesById.get(route.id)!.push(route);
307+
}
294308
for (const match of args.matches) {
295-
const rendered = renderedRouteById.get(match.route.id);
296-
if (rendered) {
297-
window.__router.patchRoutes(
298-
rendered.parentId ?? null,
299-
[createRouteFromServerManifest(rendered)],
300-
true
301-
);
309+
const renderedRoutes = renderedRoutesById.get(match.route.id);
310+
if (renderedRoutes) {
311+
for (const rendered of renderedRoutes) {
312+
window.__router.patchRoutes(
313+
rendered.parentId ?? null,
314+
[createRouteFromServerManifest(rendered)],
315+
true
316+
);
317+
}
302318
}
303319
}
304320
return results;
@@ -457,7 +473,14 @@ function createRouteFromServerManifest(
457473
let hasInitialError = payload?.errors && match.id in payload.errors;
458474
let initialError = payload?.errors?.[match.id];
459475
let isHydrationRequest =
460-
match.clientLoader?.hydrate === true || !match.hasLoader;
476+
match.clientLoader?.hydrate === true ||
477+
!match.hasLoader ||
478+
// If we don't have an element, we need to hit the server loader flow
479+
// regardless of whether the client loader calls `serverLoader` or not,
480+
// otherwise we'll have nothing to render.
481+
// TODO: Do we need to account for API routes? Do we need a
482+
// `match.hasComponent` flag?
483+
!match.element;
461484

462485
let dataRoute: DataRouteObjectWithManifestInfo = {
463486
id: match.id,

0 commit comments

Comments
 (0)