Skip to content

Commit b166e48

Browse files
authored
Minor refactors to support RSC (#13423)
1 parent 36b05eb commit b166e48

File tree

6 files changed

+129
-84
lines changed

6 files changed

+129
-84
lines changed

packages/react-router/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ export {
351351
} from "./lib/dom/ssr/routes";
352352

353353
/** @internal */
354-
export { getSingleFetchDataStrategy as UNSAFE_getSingleFetchDataStrategy } from "./lib/dom/ssr/single-fetch";
354+
export { getTurboStreamSingleFetchDataStrategy as UNSAFE_getTurboStreamSingleFetchDataStrategy } from "./lib/dom/ssr/single-fetch";
355355

356356
/** @internal */
357357
export {

packages/react-router/lib/dom-export/hydrated-router.tsx

+16-59
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
UNSAFE_createClientRoutes as createClientRoutes,
1717
UNSAFE_createRouter as createRouter,
1818
UNSAFE_deserializeErrors as deserializeErrors,
19-
UNSAFE_getSingleFetchDataStrategy as getSingleFetchDataStrategy,
19+
UNSAFE_getTurboStreamSingleFetchDataStrategy as getTurboStreamSingleFetchDataStrategy,
2020
UNSAFE_getPatchRoutesOnNavigationFunction as getPatchRoutesOnNavigationFunction,
2121
UNSAFE_shouldHydrateRouteLoader as shouldHydrateRouteLoader,
2222
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
@@ -25,6 +25,7 @@ import {
2525
UNSAFE_createClientRoutesWithHMRRevalidationOptOut as createClientRoutesWithHMRRevalidationOptOut,
2626
matchRoutes,
2727
} from "react-router";
28+
import { getHydrationData } from "../dom/ssr/hydration";
2829
import { RouterProvider } from "./dom-router-provider";
2930

3031
type SSRInfo = {
@@ -126,9 +127,9 @@ function createHydratedRouter({
126127
);
127128

128129
let hydrationData: HydrationState | undefined = undefined;
129-
let loaderData = ssrInfo.context.state.loaderData;
130130
// In SPA mode we only hydrate build-time root loader data
131131
if (ssrInfo.context.isSpaMode) {
132+
let { loaderData } = ssrInfo.context.state;
132133
if (
133134
ssrInfo.manifest.routes.root?.hasLoader &&
134135
loaderData &&
@@ -141,51 +142,19 @@ function createHydratedRouter({
141142
};
142143
}
143144
} else {
144-
// Create a shallow clone of `loaderData` we can mutate for partial hydration.
145-
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will
146-
// render the fallback so we need the client to do the same for hydration.
147-
// The server loader data has already been exposed to these route `clientLoader`'s
148-
// in `createClientRoutes` above, so we need to clear out the version we pass to
149-
// `createBrowserRouter` so it initializes and runs the client loaders.
150-
hydrationData = {
151-
...ssrInfo.context.state,
152-
loaderData: { ...loaderData },
153-
};
154-
let initialMatches = matchRoutes(
145+
hydrationData = getHydrationData(
146+
ssrInfo.context.state,
155147
routes,
148+
(routeId) => ({
149+
clientLoader: ssrInfo!.routeModules[routeId]?.clientLoader,
150+
hasLoader: ssrInfo!.manifest.routes[routeId]?.hasLoader === true,
151+
hasHydrateFallback:
152+
ssrInfo!.routeModules[routeId]?.HydrateFallback != null,
153+
}),
156154
window.location,
157-
window.__reactRouterContext?.basename
155+
window.__reactRouterContext?.basename,
156+
ssrInfo.context.isSpaMode
158157
);
159-
if (initialMatches) {
160-
for (let match of initialMatches) {
161-
let routeId = match.route.id;
162-
let route = ssrInfo.routeModules[routeId];
163-
let manifestRoute = ssrInfo.manifest.routes[routeId];
164-
// Clear out the loaderData to avoid rendering the route component when the
165-
// route opted into clientLoader hydration and either:
166-
// * gave us a HydrateFallback
167-
// * or doesn't have a server loader and we have no data to render
168-
if (
169-
route &&
170-
manifestRoute &&
171-
shouldHydrateRouteLoader(
172-
manifestRoute,
173-
route,
174-
ssrInfo.context.isSpaMode
175-
) &&
176-
(route.HydrateFallback || !manifestRoute.hasLoader)
177-
) {
178-
delete hydrationData.loaderData![routeId];
179-
} else if (manifestRoute && !manifestRoute.hasLoader) {
180-
// Since every Remix route gets a `loader` on the client side to load
181-
// the route JS module, we need to add a `null` value to `loaderData`
182-
// for any routes that don't have server loaders so our partial
183-
// hydration logic doesn't kick off the route module loaders during
184-
// hydration
185-
hydrationData.loaderData![routeId] = null;
186-
}
187-
}
188-
}
189158

190159
if (hydrationData && hydrationData.errors) {
191160
// TODO: De-dup this or remove entirely in v7 where single fetch is the
@@ -207,22 +176,10 @@ function createHydratedRouter({
207176
future: {
208177
unstable_middleware: ssrInfo.context.future.unstable_middleware,
209178
},
210-
dataStrategy: getSingleFetchDataStrategy(
179+
dataStrategy: getTurboStreamSingleFetchDataStrategy(
211180
() => router,
212-
(routeId: string) => {
213-
let manifestRoute = ssrInfo!.manifest.routes[routeId];
214-
invariant(manifestRoute, "Route not found in manifest/routeModules");
215-
let routeModule = ssrInfo!.routeModules[routeId];
216-
return {
217-
hasLoader: manifestRoute.hasLoader,
218-
hasClientLoader: manifestRoute.hasClientLoader,
219-
// In some cases the module may not be loaded yet and we don't care
220-
// if it's got shouldRevalidate or not
221-
hasShouldRevalidate: routeModule
222-
? routeModule.shouldRevalidate != null
223-
: undefined,
224-
};
225-
},
181+
ssrInfo.manifest,
182+
ssrInfo.routeModules,
226183
ssrInfo.context.ssr,
227184
ssrInfo.context.basename
228185
),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { DataRouteObject } from "../../context";
2+
import type { Path } from "../../router/history";
3+
import type { Router as DataRouter, HydrationState } from "../../router/router";
4+
import { matchRoutes } from "../../router/utils";
5+
import type { ClientLoaderFunction } from "./routeModules";
6+
import { shouldHydrateRouteLoader } from "./routes";
7+
8+
export function getHydrationData(
9+
state: {
10+
loaderData?: DataRouter["state"]["loaderData"];
11+
actionData?: DataRouter["state"]["actionData"];
12+
errors?: DataRouter["state"]["errors"];
13+
},
14+
routes: DataRouteObject[],
15+
getRouteInfo: (routeId: string) => {
16+
clientLoader: ClientLoaderFunction | undefined;
17+
hasLoader: boolean;
18+
hasHydrateFallback: boolean;
19+
},
20+
location: Path,
21+
basename: string | undefined,
22+
isSpaMode: boolean
23+
): HydrationState {
24+
// Create a shallow clone of `loaderData` we can mutate for partial hydration.
25+
// When a route exports a `clientLoader` and a `HydrateFallback`, the SSR will
26+
// render the fallback so we need the client to do the same for hydration.
27+
// The server loader data has already been exposed to these route `clientLoader`'s
28+
// in `createClientRoutes` above, so we need to clear out the version we pass to
29+
// `createBrowserRouter` so it initializes and runs the client loaders.
30+
let hydrationData = {
31+
...state,
32+
loaderData: { ...state.loaderData },
33+
};
34+
let initialMatches = matchRoutes(routes, location, basename);
35+
if (initialMatches) {
36+
for (let match of initialMatches) {
37+
let routeId = match.route.id;
38+
let routeInfo = getRouteInfo(routeId);
39+
// Clear out the loaderData to avoid rendering the route component when the
40+
// route opted into clientLoader hydration and either:
41+
// * gave us a HydrateFallback
42+
// * or doesn't have a server loader and we have no data to render
43+
if (
44+
shouldHydrateRouteLoader(
45+
routeId,
46+
routeInfo.clientLoader,
47+
routeInfo.hasLoader,
48+
isSpaMode
49+
) &&
50+
(routeInfo.hasHydrateFallback || !routeInfo.hasLoader)
51+
) {
52+
delete hydrationData.loaderData![routeId];
53+
} else if (!routeInfo.hasLoader) {
54+
// Since every Remix route gets a `loader` on the client side to load
55+
// the route JS module, we need to add a `null` value to `loaderData`
56+
// for any routes that don't have server loaders so our partial
57+
// hydration logic doesn't kick off the route module loaders during
58+
// hydration
59+
hydrationData.loaderData![routeId] = null;
60+
}
61+
}
62+
}
63+
64+
return hydrationData;
65+
}

packages/react-router/lib/dom/ssr/routes.tsx

+14-8
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import type {
99
ShouldRevalidateFunctionArgs,
1010
} from "../../router/utils";
1111
import { ErrorResponseImpl, compilePath } from "../../router/utils";
12-
import type { RouteModule, RouteModules } from "./routeModules";
12+
import type {
13+
ClientLoaderFunction,
14+
RouteModule,
15+
RouteModules,
16+
} from "./routeModules";
1317
import { loadRouteModule } from "./routeModules";
1418
import type { FutureConfig } from "./entry";
1519
import { prefetchRouteCss, prefetchStyleLinks } from "./links";
@@ -382,8 +386,9 @@ export function createClientRoutes(
382386

383387
// Let React Router know whether to run this on hydration
384388
dataRoute.loader.hydrate = shouldHydrateRouteLoader(
385-
route,
386-
routeModule,
389+
route.id,
390+
routeModule.clientLoader,
391+
route.hasLoader,
387392
isSpaMode
388393
);
389394

@@ -676,13 +681,14 @@ function getRouteModuleComponent(routeModule: RouteModule) {
676681
}
677682

678683
export function shouldHydrateRouteLoader(
679-
route: EntryRoute,
680-
routeModule: RouteModule,
684+
routeId: string,
685+
clientLoader: ClientLoaderFunction | undefined,
686+
hasLoader: boolean,
681687
isSpaMode: boolean
682688
) {
683689
return (
684-
(isSpaMode && route.id !== "root") ||
685-
(routeModule.clientLoader != null &&
686-
(routeModule.clientLoader.hydrate === true || route.hasLoader !== true))
690+
(isSpaMode && routeId !== "root") ||
691+
(clientLoader != null &&
692+
(clientLoader.hydrate === true || hasLoader !== true))
687693
);
688694
}

packages/react-router/lib/dom/ssr/server.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ export function ServerRouter({
5757
if (
5858
route &&
5959
manifestRoute &&
60-
shouldHydrateRouteLoader(manifestRoute, route, context.isSpaMode) &&
60+
shouldHydrateRouteLoader(
61+
routeId,
62+
route.clientLoader,
63+
manifestRoute.hasLoader,
64+
context.isSpaMode
65+
) &&
6166
(route.HydrateFallback || !manifestRoute.hasLoader)
6267
) {
6368
delete context.staticHandlerContext.loaderData[routeId];

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

+27-15
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ import {
1515
stripBasename,
1616
} from "../../router/utils";
1717
import { createRequestInit } from "./data";
18-
import type { EntryContext } from "./entry";
18+
import type { AssetsManifest, EntryContext } from "./entry";
1919
import { escapeHtml } from "./markup";
2020
import invariant from "./invariant";
21+
import type { RouteModules } from "./routeModules";
22+
import type { DataRouteMatch } from "../../context";
2123

2224
export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");
2325

@@ -147,10 +149,10 @@ export function StreamTransfer({
147149
}
148150
}
149151

150-
type GetRouteInfoFunction = (routeId: string) => {
152+
type GetRouteInfoFunction = (match: DataRouteMatch) => {
151153
hasLoader: boolean;
152-
hasClientLoader: boolean; // TODO: Can this be read from match.route?
153-
hasShouldRevalidate: boolean | undefined; // TODO: Can this be read from match.route?
154+
hasClientLoader: boolean;
155+
hasShouldRevalidate: boolean;
154156
};
155157

156158
type FetchAndDecodeFunction = (
@@ -159,23 +161,33 @@ type FetchAndDecodeFunction = (
159161
targetRoutes?: string[]
160162
) => Promise<{ status: number; data: DecodedSingleFetchResults }>;
161163

162-
export function getSingleFetchDataStrategy(
164+
export function getTurboStreamSingleFetchDataStrategy(
163165
getRouter: () => DataRouter,
164-
getRouteInfo: GetRouteInfoFunction,
166+
manifest: AssetsManifest,
167+
routeModules: RouteModules,
165168
ssr: boolean,
166169
basename: string | undefined
167170
): DataStrategyFunction {
168-
let dataStrategy = getSingleFetchDataStrategyImpl(
171+
let dataStrategy = getTurboStreamSingleFetchDataStrategyImpl(
169172
getRouter,
170-
getRouteInfo,
173+
(match: DataRouteMatch) => {
174+
let manifestRoute = manifest.routes[match.route.id];
175+
invariant(manifestRoute, "Route not found in manifest");
176+
let routeModule = routeModules[match.route.id];
177+
return {
178+
hasLoader: manifestRoute.hasLoader,
179+
hasClientLoader: manifestRoute.hasClientLoader,
180+
hasShouldRevalidate: Boolean(routeModule?.shouldRevalidate),
181+
};
182+
},
171183
fetchAndDecodeViaTurboStream,
172184
ssr,
173185
basename
174186
);
175187
return async (args) => args.unstable_runClientMiddleware(dataStrategy);
176188
}
177189

178-
export function getSingleFetchDataStrategyImpl(
190+
export function getTurboStreamSingleFetchDataStrategyImpl(
179191
getRouter: () => DataRouter,
180192
getRouteInfo: GetRouteInfoFunction,
181193
fetchAndDecode: FetchAndDecodeFunction,
@@ -192,7 +204,7 @@ export function getSingleFetchDataStrategyImpl(
192204
}
193205

194206
let foundRevalidatingServerLoader = matches.some((m) => {
195-
let { hasLoader, hasClientLoader } = getRouteInfo(m.route.id);
207+
let { hasLoader, hasClientLoader } = getRouteInfo(m);
196208
return m.unstable_shouldCallHandler() && hasLoader && !hasClientLoader;
197209
});
198210
if (!ssr && !foundRevalidatingServerLoader) {
@@ -298,7 +310,7 @@ async function nonSsrStrategy(
298310
matchesToLoad.map((m) =>
299311
m.resolve(async (handler) => {
300312
try {
301-
let { hasClientLoader } = getRouteInfo(m.route.id);
313+
let { hasClientLoader } = getRouteInfo(m);
302314
// Need to pass through a `singleFetch` override handler so
303315
// clientLoader's can still call server loaders through `.data`
304316
// requests
@@ -350,7 +362,7 @@ async function singleFetchLoaderNavigationStrategy(
350362
routeDfds[i].resolve();
351363
let routeId = m.route.id;
352364
let { hasLoader, hasClientLoader, hasShouldRevalidate } =
353-
getRouteInfo(routeId);
365+
getRouteInfo(m);
354366

355367
let defaultShouldRevalidate =
356368
!m.unstable_shouldRevalidateArgs ||
@@ -419,7 +431,7 @@ async function singleFetchLoaderNavigationStrategy(
419431
(!router.state.initialized || routesParams.size === 0) &&
420432
!window.__reactRouterHdrActive
421433
) {
422-
singleFetchDfd.resolve({});
434+
singleFetchDfd.resolve({ routes: {} });
423435
} else {
424436
// When routes have opted out, add a `_routes` param to filter server loaders
425437
// Skipped in `ssr:false` because we expect to be loading static `.data` files
@@ -659,8 +671,8 @@ function unwrapSingleFetchResult(
659671
}
660672

661673
function createDeferred<T = unknown>() {
662-
let resolve: (val?: any) => Promise<void>;
663-
let reject: (error?: unknown) => Promise<void>;
674+
let resolve: (val: T) => Promise<void>;
675+
let reject: (error: unknown) => Promise<void>;
664676
let promise = new Promise<T>((res, rej) => {
665677
resolve = async (val: T) => {
666678
res(val);

0 commit comments

Comments
 (0)