Skip to content

Commit bd4ac8e

Browse files
authored
Call patchRoutesOnMiss when matching slug routes in case there exist higher scoring static routes (#11883)
1 parent de4e366 commit bd4ac8e

File tree

3 files changed

+365
-42
lines changed

3 files changed

+365
-42
lines changed

.changeset/cyan-bobcats-notice.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@remix-run/router": patch
3+
---
4+
5+
Fog of War: Update `unstable_patchRoutesOnMiss` logic so that we call the method when we match routes with dynamic param or splat segments in case there exists a higher-scoring static route that we've not yet discovered.
6+
7+
- We also now leverage an internal FIFO queue of previous paths we've already called `unstable_patchRouteOnMiss` against so that we don't re-call on subsequent navigations to the same path

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

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,121 @@ describe("Lazy Route Discovery (Fog of War)", () => {
507507
]);
508508
});
509509

510+
it("de-prioritizes dynamic param routes in favor of looking for better async matches", async () => {
511+
router = createRouter({
512+
history: createMemoryHistory(),
513+
routes: [
514+
{
515+
path: "/",
516+
},
517+
{
518+
id: "slug",
519+
path: "/:slug",
520+
},
521+
],
522+
async unstable_patchRoutesOnMiss({ patch }) {
523+
await tick();
524+
patch(null, [
525+
{
526+
id: "static",
527+
path: "/static",
528+
},
529+
]);
530+
},
531+
});
532+
533+
await router.navigate("/static");
534+
expect(router.state.location.pathname).toBe("/static");
535+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
536+
});
537+
538+
it("de-prioritizes dynamic param routes in favor of looking for better async matches (product/:slug)", async () => {
539+
router = createRouter({
540+
history: createMemoryHistory(),
541+
routes: [
542+
{
543+
path: "/",
544+
},
545+
{
546+
id: "slug",
547+
path: "/product/:slug",
548+
},
549+
],
550+
async unstable_patchRoutesOnMiss({ patch }) {
551+
await tick();
552+
patch(null, [
553+
{
554+
id: "static",
555+
path: "/product/static",
556+
},
557+
]);
558+
},
559+
});
560+
561+
await router.navigate("/product/static");
562+
expect(router.state.location.pathname).toBe("/product/static");
563+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
564+
});
565+
566+
it("de-prioritizes dynamic param routes in favor of looking for better async matches (child route)", async () => {
567+
router = createRouter({
568+
history: createMemoryHistory(),
569+
routes: [
570+
{
571+
path: "/",
572+
},
573+
{
574+
id: "product",
575+
path: "/product",
576+
children: [
577+
{
578+
id: "slug",
579+
path: ":slug",
580+
},
581+
],
582+
},
583+
],
584+
async unstable_patchRoutesOnMiss({ patch }) {
585+
await tick();
586+
patch("product", [
587+
{
588+
id: "static",
589+
path: "static",
590+
},
591+
]);
592+
},
593+
});
594+
595+
await router.navigate("/product/static");
596+
expect(router.state.location.pathname).toBe("/product/static");
597+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
598+
"product",
599+
"static",
600+
]);
601+
});
602+
603+
it("matches dynamic params when other paths don't pan out", async () => {
604+
router = createRouter({
605+
history: createMemoryHistory(),
606+
routes: [
607+
{
608+
path: "/",
609+
},
610+
{
611+
id: "slug",
612+
path: "/:slug",
613+
},
614+
],
615+
async unstable_patchRoutesOnMiss({ matches, patch }) {
616+
await tick();
617+
},
618+
});
619+
620+
await router.navigate("/a");
621+
expect(router.state.location.pathname).toBe("/a");
622+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["slug"]);
623+
});
624+
510625
it("de-prioritizes splat routes in favor of looking for better async matches", async () => {
511626
router = createRouter({
512627
history: createMemoryHistory(),
@@ -569,6 +684,43 @@ describe("Lazy Route Discovery (Fog of War)", () => {
569684
expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
570685
});
571686

687+
it("de-prioritizes splat routes in favor of looking for better async matches (child route)", async () => {
688+
router = createRouter({
689+
history: createMemoryHistory(),
690+
routes: [
691+
{
692+
path: "/",
693+
},
694+
{
695+
id: "product",
696+
path: "/product",
697+
children: [
698+
{
699+
id: "splat",
700+
path: "*",
701+
},
702+
],
703+
},
704+
],
705+
async unstable_patchRoutesOnMiss({ patch }) {
706+
await tick();
707+
patch("product", [
708+
{
709+
id: "static",
710+
path: "static",
711+
},
712+
]);
713+
},
714+
});
715+
716+
await router.navigate("/product/static");
717+
expect(router.state.location.pathname).toBe("/product/static");
718+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
719+
"product",
720+
"static",
721+
]);
722+
});
723+
572724
it("matches splats when other paths don't pan out", async () => {
573725
router = createRouter({
574726
history: createMemoryHistory(),
@@ -603,6 +755,50 @@ describe("Lazy Route Discovery (Fog of War)", () => {
603755
expect(router.state.matches.map((m) => m.route.id)).toEqual(["splat"]);
604756
});
605757

758+
it("recurses unstable_patchRoutesOnMiss until a match is found", async () => {
759+
let count = 0;
760+
router = createRouter({
761+
history: createMemoryHistory(),
762+
routes: [
763+
{
764+
path: "/",
765+
},
766+
{
767+
id: "a",
768+
path: "a",
769+
},
770+
],
771+
async unstable_patchRoutesOnMiss({ matches, patch }) {
772+
await tick();
773+
count++;
774+
if (last(matches).route.id === "a") {
775+
patch("a", [
776+
{
777+
id: "b",
778+
path: "b",
779+
},
780+
]);
781+
} else if (last(matches).route.id === "b") {
782+
patch("b", [
783+
{
784+
id: "c",
785+
path: "c",
786+
},
787+
]);
788+
}
789+
},
790+
});
791+
792+
await router.navigate("/a/b/c");
793+
expect(router.state.location.pathname).toBe("/a/b/c");
794+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
795+
"a",
796+
"b",
797+
"c",
798+
]);
799+
expect(count).toBe(2);
800+
});
801+
606802
it("discovers routes during initial hydration", async () => {
607803
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();
608804
let loaderDfd = createDeferred();
@@ -1063,6 +1259,136 @@ describe("Lazy Route Discovery (Fog of War)", () => {
10631259
unsubscribe();
10641260
});
10651261

1262+
it('does not re-call for previously called "good" paths', async () => {
1263+
let count = 0;
1264+
router = createRouter({
1265+
history: createMemoryHistory(),
1266+
routes: [
1267+
{
1268+
path: "/",
1269+
},
1270+
{
1271+
id: "param",
1272+
path: ":param",
1273+
},
1274+
],
1275+
async unstable_patchRoutesOnMiss() {
1276+
count++;
1277+
await tick();
1278+
// Nothing to patch - there is no better static route in this case
1279+
},
1280+
});
1281+
1282+
await router.navigate("/whatever");
1283+
expect(count).toBe(1);
1284+
expect(router.state.location.pathname).toBe("/whatever");
1285+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
1286+
1287+
await router.navigate("/");
1288+
expect(count).toBe(1);
1289+
expect(router.state.location.pathname).toBe("/");
1290+
1291+
await router.navigate("/whatever");
1292+
expect(count).toBe(1); // Not called again
1293+
expect(router.state.location.pathname).toBe("/whatever");
1294+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
1295+
});
1296+
1297+
it("does not re-call for previously called 404 paths", async () => {
1298+
let count = 0;
1299+
router = createRouter({
1300+
history: createMemoryHistory(),
1301+
routes: [
1302+
{
1303+
id: "index",
1304+
path: "/",
1305+
},
1306+
{
1307+
id: "static",
1308+
path: "static",
1309+
},
1310+
],
1311+
async unstable_patchRoutesOnMiss() {
1312+
count++;
1313+
},
1314+
});
1315+
1316+
await router.navigate("/junk");
1317+
expect(count).toBe(1);
1318+
expect(router.state.location.pathname).toBe("/junk");
1319+
expect(router.state.errors?.index).toEqual(
1320+
new ErrorResponseImpl(
1321+
404,
1322+
"Not Found",
1323+
new Error('No route matches URL "/junk"'),
1324+
true
1325+
)
1326+
);
1327+
1328+
await router.navigate("/");
1329+
expect(count).toBe(1);
1330+
expect(router.state.location.pathname).toBe("/");
1331+
expect(router.state.errors).toBeNull();
1332+
1333+
await router.navigate("/junk");
1334+
expect(count).toBe(1);
1335+
expect(router.state.location.pathname).toBe("/junk");
1336+
expect(router.state.errors?.index).toEqual(
1337+
new ErrorResponseImpl(
1338+
404,
1339+
"Not Found",
1340+
new Error('No route matches URL "/junk"'),
1341+
true
1342+
)
1343+
);
1344+
});
1345+
1346+
it("caps internal fifo queue at 1000 paths", async () => {
1347+
let count = 0;
1348+
router = createRouter({
1349+
history: createMemoryHistory(),
1350+
routes: [
1351+
{
1352+
path: "/",
1353+
},
1354+
{
1355+
id: "param",
1356+
path: ":param",
1357+
},
1358+
],
1359+
async unstable_patchRoutesOnMiss() {
1360+
count++;
1361+
// Nothing to patch - there is no better static route in this case
1362+
},
1363+
});
1364+
1365+
// Fill it up with 1000 paths
1366+
for (let i = 1; i <= 1000; i++) {
1367+
await router.navigate(`/path-${i}`);
1368+
expect(count).toBe(i);
1369+
expect(router.state.location.pathname).toBe(`/path-${i}`);
1370+
1371+
await router.navigate("/");
1372+
expect(count).toBe(i);
1373+
expect(router.state.location.pathname).toBe("/");
1374+
}
1375+
1376+
// Don't call patchRoutesOnMiss since this is the first item in the queue
1377+
await router.navigate(`/path-1`);
1378+
expect(count).toBe(1000);
1379+
expect(router.state.location.pathname).toBe(`/path-1`);
1380+
1381+
// Call patchRoutesOnMiss and evict the first item
1382+
await router.navigate(`/path-1001`);
1383+
expect(count).toBe(1001);
1384+
expect(router.state.location.pathname).toBe(`/path-1001`);
1385+
1386+
// Call patchRoutesOnMiss since this item was evicted
1387+
await router.navigate(`/path-1`);
1388+
expect(count).toBe(1002);
1389+
expect(router.state.location.pathname).toBe(`/path-1`);
1390+
});
1391+
10661392
describe("errors", () => {
10671393
it("lazy 404s (GET navigation)", async () => {
10681394
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();

0 commit comments

Comments
 (0)