Skip to content

Commit eb30a88

Browse files
authored
Trigger a new router.routes identity during fog of war route patching (#11740)
1 parent dd607e0 commit eb30a88

File tree

3 files changed

+127
-18
lines changed

3 files changed

+127
-18
lines changed

.changeset/lovely-apples-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Trigger a new `router.routes` identity/reflow during fog of war route patching

packages/router/__tests__/lazy-discovery-test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,6 +835,80 @@ describe("Lazy Route Discovery (Fog of War)", () => {
835835
]);
836836
});
837837

838+
it("creates a new router.routes identity when patching routes", async () => {
839+
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();
840+
841+
router = createRouter({
842+
history: createMemoryHistory(),
843+
routes: [
844+
{
845+
path: "/",
846+
},
847+
{
848+
id: "parent",
849+
path: "parent",
850+
},
851+
],
852+
async unstable_patchRoutesOnMiss({ patch }) {
853+
let children = await childrenDfd.promise;
854+
patch("parent", children);
855+
},
856+
});
857+
let originalRoutes = router.routes;
858+
859+
router.navigate("/parent/child");
860+
childrenDfd.resolve([
861+
{
862+
id: "child",
863+
path: "child",
864+
},
865+
]);
866+
await tick();
867+
868+
expect(router.state.location.pathname).toBe("/parent/child");
869+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
870+
"parent",
871+
"child",
872+
]);
873+
874+
expect(router.routes).not.toBe(originalRoutes);
875+
});
876+
877+
it("allows patching externally/eagerly and triggers a reflow", async () => {
878+
router = createRouter({
879+
history: createMemoryHistory(),
880+
routes: [
881+
{
882+
path: "/",
883+
},
884+
{
885+
id: "parent",
886+
path: "parent",
887+
},
888+
],
889+
});
890+
let spy = jest.fn();
891+
let unsubscribe = router.subscribe(spy);
892+
let originalRoutes = router.routes;
893+
router.patchRoutes("parent", [
894+
{
895+
id: "child",
896+
path: "child",
897+
},
898+
]);
899+
expect(spy).toHaveBeenCalled();
900+
expect(router.routes).not.toBe(originalRoutes);
901+
902+
await router.navigate("/parent/child");
903+
expect(router.state.location.pathname).toBe("/parent/child");
904+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
905+
"parent",
906+
"child",
907+
]);
908+
909+
unsubscribe();
910+
});
911+
838912
describe("errors", () => {
839913
it("lazy 404s (GET navigation)", async () => {
840914
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();

packages/router/router.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ export interface Router {
249249
* PRIVATE DO NOT USE
250250
*
251251
* Patch additional children routes into an existing parent route
252-
* @param routeId The parent route id
252+
* @param routeId The parent route id or a callback function accepting `patch`
253+
* to perform batch patching
253254
* @param children The additional children routes
254255
*/
255256
patchRoutes(routeId: string | null, children: AgnosticRouteObject[]): void;
@@ -1222,6 +1223,7 @@ export function createRouter(init: RouterInit): Router {
12221223
isMutationMethod(state.navigation.formMethod) &&
12231224
location.state?._isRedirect !== true);
12241225

1226+
// Commit any in-flight routes at the end of the HMR revalidation "navigation"
12251227
if (inFlightDataRoutes) {
12261228
dataRoutes = inFlightDataRoutes;
12271229
inFlightDataRoutes = undefined;
@@ -3204,26 +3206,37 @@ export function createRouter(init: RouterInit): Router {
32043206
? partialMatches[partialMatches.length - 1].route
32053207
: null;
32063208
while (true) {
3209+
let isNonHMR = inFlightDataRoutes == null;
3210+
let routesToUse = inFlightDataRoutes || dataRoutes;
32073211
try {
32083212
await loadLazyRouteChildren(
32093213
patchRoutesOnMissImpl!,
32103214
pathname,
32113215
partialMatches,
3212-
dataRoutes || inFlightDataRoutes,
3216+
routesToUse,
32133217
manifest,
32143218
mapRouteProperties,
32153219
pendingPatchRoutes,
32163220
signal
32173221
);
32183222
} catch (e) {
32193223
return { type: "error", error: e, partialMatches };
3224+
} finally {
3225+
// If we are not in the middle of an HMR revalidation and we changed the
3226+
// routes, provide a new identity so when we `updateState` at the end of
3227+
// this navigation/fetch `router.routes` will be a new identity and
3228+
// trigger a re-run of memoized `router.routes` dependencies.
3229+
// HMR will already update the identity and reflow when it lands
3230+
// `inFlightDataRoutes` in `completeNavigation`
3231+
if (isNonHMR) {
3232+
dataRoutes = [...dataRoutes];
3233+
}
32203234
}
32213235

32223236
if (signal.aborted) {
32233237
return { type: "aborted" };
32243238
}
32253239

3226-
let routesToUse = inFlightDataRoutes || dataRoutes;
32273240
let newMatches = matchRoutes(routesToUse, pathname, basename);
32283241
let matchedSplat = false;
32293242
if (newMatches) {
@@ -3284,6 +3297,31 @@ export function createRouter(init: RouterInit): Router {
32843297
);
32853298
}
32863299

3300+
function patchRoutes(
3301+
routeId: string | null,
3302+
children: AgnosticRouteObject[]
3303+
): void {
3304+
let isNonHMR = inFlightDataRoutes == null;
3305+
let routesToUse = inFlightDataRoutes || dataRoutes;
3306+
patchRoutesImpl(
3307+
routeId,
3308+
children,
3309+
routesToUse,
3310+
manifest,
3311+
mapRouteProperties
3312+
);
3313+
3314+
// If we are not in the middle of an HMR revalidation and we changed the
3315+
// routes, provide a new identity and trigger a reflow via `updateState`
3316+
// to re-run memoized `router.routes` dependencies.
3317+
// HMR will already update the identity and reflow when it lands
3318+
// `inFlightDataRoutes` in `completeNavigation`
3319+
if (isNonHMR) {
3320+
dataRoutes = [...dataRoutes];
3321+
updateState({});
3322+
}
3323+
}
3324+
32873325
router = {
32883326
get basename() {
32893327
return basename;
@@ -3315,15 +3353,7 @@ export function createRouter(init: RouterInit): Router {
33153353
dispose,
33163354
getBlocker,
33173355
deleteBlocker,
3318-
patchRoutes(routeId, children) {
3319-
return patchRoutes(
3320-
routeId,
3321-
children,
3322-
dataRoutes || inFlightDataRoutes,
3323-
manifest,
3324-
mapRouteProperties
3325-
);
3326-
},
3356+
patchRoutes,
33273357
_internalFetchControllers: fetchControllers,
33283358
_internalActiveDeferreds: activeDeferreds,
33293359
// TODO: Remove setRoutes, it's temporary to avoid dealing with
@@ -4488,7 +4518,7 @@ function shouldRevalidateLoader(
44884518
}
44894519

44904520
/**
4491-
* Idempotent utility to execute route.children() method to lazily load route
4521+
* Idempotent utility to execute patchRoutesOnMiss() to lazily load route
44924522
* definitions and update the routes/routeManifest
44934523
*/
44944524
async function loadLazyRouteChildren(
@@ -4510,7 +4540,7 @@ async function loadLazyRouteChildren(
45104540
matches,
45114541
patch: (routeId, children) => {
45124542
if (!signal.aborted) {
4513-
patchRoutes(
4543+
patchRoutesImpl(
45144544
routeId,
45154545
children,
45164546
routes,
@@ -4531,10 +4561,10 @@ async function loadLazyRouteChildren(
45314561
}
45324562
}
45334563

4534-
function patchRoutes(
4564+
function patchRoutesImpl(
45354565
routeId: string | null,
45364566
children: AgnosticRouteObject[],
4537-
routes: AgnosticDataRouteObject[],
4567+
routesToUse: AgnosticDataRouteObject[],
45384568
manifest: RouteManifest,
45394569
mapRouteProperties: MapRoutePropertiesFunction
45404570
) {
@@ -4559,10 +4589,10 @@ function patchRoutes(
45594589
let dataChildren = convertRoutesToDataRoutes(
45604590
children,
45614591
mapRouteProperties,
4562-
["patch", String(routes.length || "0")],
4592+
["patch", String(routesToUse.length || "0")],
45634593
manifest
45644594
);
4565-
routes.push(...dataChildren);
4595+
routesToUse.push(...dataChildren);
45664596
}
45674597
}
45684598

0 commit comments

Comments
 (0)