Skip to content

Commit 0471c02

Browse files
authored
Fix idempotent patchRoutes for sibling pathless layout routes (#12013)
1 parent 9f1db94 commit 0471c02

File tree

2 files changed

+233
-6
lines changed

2 files changed

+233
-6
lines changed

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

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,196 @@ describe("Lazy Route Discovery (Fog of War)", () => {
13301330
`);
13311331
});
13321332

1333+
it("distinguishes sibling pathless layout routes in idempotent patch check (via id)", async () => {
1334+
let count = 0;
1335+
router = createRouter({
1336+
history: createMemoryHistory(),
1337+
routes: [
1338+
{
1339+
id: "root",
1340+
path: "/",
1341+
children: [
1342+
{
1343+
id: "a-layout",
1344+
children: [
1345+
{
1346+
id: "a",
1347+
path: "a",
1348+
},
1349+
],
1350+
},
1351+
],
1352+
},
1353+
],
1354+
async patchRoutesOnNavigation({ patch, path }) {
1355+
count++;
1356+
if (path === "/b") {
1357+
patch("root", [
1358+
{
1359+
id: "b-layout",
1360+
children: [
1361+
{
1362+
id: "b",
1363+
path: "b",
1364+
},
1365+
],
1366+
},
1367+
]);
1368+
}
1369+
await tick();
1370+
},
1371+
});
1372+
1373+
await router.navigate("/a");
1374+
expect(router.state.location.pathname).toBe("/a");
1375+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
1376+
"root",
1377+
"a-layout",
1378+
"a",
1379+
]);
1380+
expect(router.state.errors).toBeNull();
1381+
expect(count).toBe(0);
1382+
1383+
await router.navigate("/b");
1384+
expect(router.state.location.pathname).toBe("/b");
1385+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
1386+
"root",
1387+
"b-layout",
1388+
"b",
1389+
]);
1390+
expect(router.state.errors).toBeNull();
1391+
expect(count).toBe(1);
1392+
1393+
expect(router.routes).toMatchInlineSnapshot(`
1394+
[
1395+
{
1396+
"children": [
1397+
{
1398+
"children": [
1399+
{
1400+
"children": undefined,
1401+
"hasErrorBoundary": false,
1402+
"id": "a",
1403+
"path": "a",
1404+
},
1405+
],
1406+
"hasErrorBoundary": false,
1407+
"id": "a-layout",
1408+
},
1409+
{
1410+
"children": [
1411+
{
1412+
"children": undefined,
1413+
"hasErrorBoundary": false,
1414+
"id": "b",
1415+
"path": "b",
1416+
},
1417+
],
1418+
"hasErrorBoundary": false,
1419+
"id": "b-layout",
1420+
},
1421+
],
1422+
"hasErrorBoundary": false,
1423+
"id": "root",
1424+
"path": "/",
1425+
},
1426+
]
1427+
`);
1428+
});
1429+
1430+
it("distinguishes sibling pathless layout routes in idempotent patch check (via children)", async () => {
1431+
let count = 0;
1432+
router = createRouter({
1433+
history: createMemoryHistory(),
1434+
routes: [
1435+
{
1436+
id: "root",
1437+
path: "/",
1438+
children: [
1439+
{
1440+
children: [
1441+
{
1442+
path: "a",
1443+
},
1444+
],
1445+
},
1446+
],
1447+
},
1448+
],
1449+
async patchRoutesOnNavigation({ patch, path }) {
1450+
count++;
1451+
if (path === "/b") {
1452+
patch("root", [
1453+
{
1454+
children: [
1455+
{
1456+
path: "b",
1457+
},
1458+
],
1459+
},
1460+
]);
1461+
}
1462+
await tick();
1463+
},
1464+
});
1465+
1466+
await router.navigate("/a");
1467+
expect(router.state.location.pathname).toBe("/a");
1468+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
1469+
"root",
1470+
"0-0",
1471+
"0-0-0",
1472+
]);
1473+
expect(router.state.errors).toBeNull();
1474+
expect(count).toBe(0);
1475+
1476+
await router.navigate("/b");
1477+
expect(router.state.location.pathname).toBe("/b");
1478+
expect(router.state.matches.map((m) => m.route.id)).toEqual([
1479+
"root",
1480+
"root-patch-1-0",
1481+
"root-patch-1-0-0",
1482+
]);
1483+
expect(router.state.errors).toBeNull();
1484+
expect(count).toBe(1);
1485+
1486+
expect(router.routes).toMatchInlineSnapshot(`
1487+
[
1488+
{
1489+
"children": [
1490+
{
1491+
"children": [
1492+
{
1493+
"children": undefined,
1494+
"hasErrorBoundary": false,
1495+
"id": "0-0-0",
1496+
"path": "a",
1497+
},
1498+
],
1499+
"hasErrorBoundary": false,
1500+
"id": "0-0",
1501+
},
1502+
{
1503+
"children": [
1504+
{
1505+
"children": undefined,
1506+
"hasErrorBoundary": false,
1507+
"id": "root-patch-1-0-0",
1508+
"path": "b",
1509+
},
1510+
],
1511+
"hasErrorBoundary": false,
1512+
"id": "root-patch-1-0",
1513+
},
1514+
],
1515+
"hasErrorBoundary": false,
1516+
"id": "root",
1517+
"path": "/",
1518+
},
1519+
]
1520+
`);
1521+
});
1522+
13331523
describe("errors", () => {
13341524
it("lazy 404s (GET navigation)", async () => {
13351525
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();

packages/router/router.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4677,12 +4677,9 @@ function patchRoutesImpl(
46774677
// to simplify user-land code. This is useful because we re-call the
46784678
// `patchRoutesOnNavigation` function for matched routes with params.
46794679
let uniqueChildren = children.filter(
4680-
(a) =>
4681-
!childrenToPatch.some(
4682-
(b) =>
4683-
a.index === b.index &&
4684-
a.path === b.path &&
4685-
a.caseSensitive === b.caseSensitive
4680+
(newRoute) =>
4681+
!childrenToPatch.some((existingRoute) =>
4682+
isSameRoute(newRoute, existingRoute)
46864683
)
46874684
);
46884685

@@ -4696,6 +4693,46 @@ function patchRoutesImpl(
46964693
childrenToPatch.push(...newRoutes);
46974694
}
46984695

4696+
function isSameRoute(
4697+
newRoute: AgnosticRouteObject,
4698+
existingRoute: AgnosticRouteObject
4699+
): boolean {
4700+
// Most optimal check is by id
4701+
if (
4702+
"id" in newRoute &&
4703+
"id" in existingRoute &&
4704+
newRoute.id === existingRoute.id
4705+
) {
4706+
return true;
4707+
}
4708+
4709+
// Second is by pathing differences
4710+
if (
4711+
!(
4712+
newRoute.index === existingRoute.index &&
4713+
newRoute.path === existingRoute.path &&
4714+
newRoute.caseSensitive === existingRoute.caseSensitive
4715+
)
4716+
) {
4717+
return false;
4718+
}
4719+
4720+
// Pathless layout routes are trickier since we need to check children.
4721+
// If they have no children then they're the same as far as we can tell
4722+
if (
4723+
(!newRoute.children || newRoute.children.length === 0) &&
4724+
(!existingRoute.children || existingRoute.children.length === 0)
4725+
) {
4726+
return true;
4727+
}
4728+
4729+
// Otherwise, we look to see if every child in the new route is already
4730+
// represented in the existing route's children
4731+
return newRoute.children!.every((aChild, i) =>
4732+
existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild))
4733+
);
4734+
}
4735+
46994736
/**
47004737
* Execute route.lazy() methods to lazily load route modules (loader, action,
47014738
* shouldRevalidate) and update the routeManifest in place which shares objects

0 commit comments

Comments
 (0)