Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-mirrors-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Fix issue with splat routes interfering with multiple calls to patchRoutesOnNavigation
107 changes: 107 additions & 0 deletions packages/react-router/__tests__/router/lazy-discovery-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,58 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});

it("discovers child routes at a depth >1 when a separate matching param route exists (GET navigation)", async () => {
router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
},
{
id: "a",
path: "a",
handle: {
async lazyChildren() {
await tick();
return [
{
id: "b",
path: "b",
handle: {
async lazyChildren() {
await tick();
return [{ id: "c", path: "c" }];
},
},
},
];
},
},
},
{
id: "splat",
path: "*",
},
],
async patchRoutesOnNavigation({ patch, matches }) {
await tick();
const leafRoute = last(matches).route;
if (leafRoute.handle?.lazyChildren) {
const children = await leafRoute.handle.lazyChildren();
patch(leafRoute.id, children);
}
},
});

await router.navigate("/a/b/c");
expect(router.state.location.pathname).toBe("/a/b/c");
expect(router.state.matches.map((m) => m.route.id)).toEqual([
"a",
"b",
"c",
]);
});

it("discovers child route at a depth of 1 (POST navigation)", async () => {
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();
let loaderDfd = createDeferred();
Expand Down Expand Up @@ -272,6 +324,61 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});

it("discovers child routes at a depth >1 when a separate matching param route exists (POST navigation)", async () => {
router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
},
{
id: "a",
path: "a",
handle: {
async lazyChildren() {
await tick();
return [
{
id: "b",
path: "b",
handle: {
async lazyChildren() {
await tick();
return [{ id: "c", path: "c" }];
},
},
},
];
},
},
},
{
id: "splat",
path: "*",
},
],
async patchRoutesOnNavigation({ patch, matches }) {
await tick();
const leafRoute = last(matches).route;
if (leafRoute.handle?.lazyChildren) {
const children = await leafRoute.handle.lazyChildren();
patch(leafRoute.id, children);
}
},
});

await router.navigate("/a/b/c", {
formMethod: "POST",
formData: createFormData({}),
});
expect(router.state.location.pathname).toBe("/a/b/c");
expect(router.state.matches.map((m) => m.route.id)).toEqual([
"a",
"b",
"c",
]);
});

it("does not reuse former calls to patchRoutes on interruptions", async () => {
let aDfd = createDeferred<AgnosticDataRouteObject[]>();
let calls: string[][] = [];
Expand Down
59 changes: 48 additions & 11 deletions packages/react-router/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3433,24 +3433,52 @@ export function createRouter(init: RouterInit): Router {
}

let newMatches = matchRoutes(routesToUse, pathname, basename);
let newPartialMatches: AgnosticDataRouteMatch[] | null = null;

if (newMatches) {
return { type: "success", matches: newMatches };
if (Object.keys(newMatches[0].params).length === 0) {
// Static match - use it
return { type: "success", matches: newMatches };
} else {
// Dynamic match - confirm this is the best match.
newPartialMatches = matchRoutesImpl(
routesToUse,
pathname,
basename,
true,
);

// If we matched deeper into the same branch of `partialMatches` we were already
// checking, we want to make another pass through `patchRoutesOnNavigation()`
let matchedDeeper =
newPartialMatches &&
partialMatches.length < newPartialMatches.length &&
compareMatches(
partialMatches,
newPartialMatches.slice(0, partialMatches.length),
);

if (!matchedDeeper) {
// Otherwise, use the dynamic matches
return { type: "success", matches: newMatches };
}
}
}

let newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
routesToUse,
pathname,
basename,
true,
);
// Perform partial matching if we didn't already do it above
if (!newPartialMatches) {
newPartialMatches = matchRoutesImpl<AgnosticDataRouteObject>(
routesToUse,
pathname,
basename,
true,
);
}

// Avoid loops if the second pass results in the same partial matches
if (
!newPartialMatches ||
(partialMatches.length === newPartialMatches.length &&
partialMatches.every(
(m, i) => m.route.id === newPartialMatches![i].route.id,
))
compareMatches(partialMatches, newPartialMatches)
) {
return { type: "success", matches: null };
}
Expand All @@ -3459,6 +3487,15 @@ export function createRouter(init: RouterInit): Router {
}
}

function compareMatches(
a: AgnosticDataRouteMatch[],
b: AgnosticDataRouteMatch[],
) {
return (
a.length === b.length && a.every((m, i) => m.route.id === b[i].route.id)
);
}

function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
manifest = {};
inFlightDataRoutes = convertRoutesToDataRoutes(
Expand Down