Skip to content

Commit aa37dd1

Browse files
authored
Remove internal discoverRoutes queue and make patch idempotent (#11978)
1 parent 0fc83f8 commit aa37dd1

File tree

2 files changed

+68
-138
lines changed

2 files changed

+68
-138
lines changed

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

+41-100
Original file line numberDiff line numberDiff line change
@@ -1202,134 +1202,75 @@ describe("Lazy Route Discovery (Fog of War)", () => {
12021202
unsubscribe();
12031203
});
12041204

1205-
it('does not re-call for previously called "good" paths', async () => {
1205+
it("does not re-patch previously patched routes", async () => {
12061206
let count = 0;
12071207
router = createRouter({
12081208
history: createMemoryHistory(),
12091209
routes: [
12101210
{
12111211
path: "/",
12121212
},
1213-
{
1214-
id: "param",
1215-
path: ":param",
1216-
},
12171213
],
1218-
async patchRoutesOnNavigation() {
1214+
async patchRoutesOnNavigation({ patch }) {
12191215
count++;
1216+
patch(null, [
1217+
{
1218+
id: "param",
1219+
path: ":param",
1220+
},
1221+
]);
12201222
await tick();
1221-
// Nothing to patch - there is no better static route in this case
12221223
},
12231224
});
12241225

1225-
await router.navigate("/whatever");
1226-
expect(count).toBe(1);
1227-
expect(router.state.location.pathname).toBe("/whatever");
1226+
await router.navigate("/a");
1227+
expect(router.state.location.pathname).toBe("/a");
12281228
expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
1229-
1230-
await router.navigate("/");
12311229
expect(count).toBe(1);
1232-
expect(router.state.location.pathname).toBe("/");
1233-
1234-
await router.navigate("/whatever");
1235-
expect(count).toBe(1); // Not called again
1236-
expect(router.state.location.pathname).toBe("/whatever");
1237-
expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
1238-
});
1239-
1240-
it("does not re-call for previously called 404 paths", async () => {
1241-
let count = 0;
1242-
router = createRouter({
1243-
history: createMemoryHistory(),
1244-
routes: [
1230+
expect(router.routes).toMatchInlineSnapshot(`
1231+
[
12451232
{
1246-
id: "index",
1247-
path: "/",
1233+
"children": undefined,
1234+
"hasErrorBoundary": false,
1235+
"id": "0",
1236+
"path": "/",
12481237
},
12491238
{
1250-
id: "static",
1251-
path: "static",
1239+
"children": undefined,
1240+
"hasErrorBoundary": false,
1241+
"id": "param",
1242+
"path": ":param",
12521243
},
1253-
],
1254-
async patchRoutesOnNavigation() {
1255-
count++;
1256-
},
1257-
});
1258-
1259-
await router.navigate("/junk");
1260-
expect(count).toBe(1);
1261-
expect(router.state.location.pathname).toBe("/junk");
1262-
expect(router.state.errors?.index).toEqual(
1263-
new ErrorResponseImpl(
1264-
404,
1265-
"Not Found",
1266-
new Error('No route matches URL "/junk"'),
1267-
true
1268-
)
1269-
);
1244+
]
1245+
`);
12701246

12711247
await router.navigate("/");
1272-
expect(count).toBe(1);
12731248
expect(router.state.location.pathname).toBe("/");
1274-
expect(router.state.errors).toBeNull();
1275-
1276-
await router.navigate("/junk");
12771249
expect(count).toBe(1);
1278-
expect(router.state.location.pathname).toBe("/junk");
1279-
expect(router.state.errors?.index).toEqual(
1280-
new ErrorResponseImpl(
1281-
404,
1282-
"Not Found",
1283-
new Error('No route matches URL "/junk"'),
1284-
true
1285-
)
1286-
);
1287-
});
12881250

1289-
it("caps internal fifo queue at 1000 paths", async () => {
1290-
let count = 0;
1291-
router = createRouter({
1292-
history: createMemoryHistory(),
1293-
routes: [
1251+
await router.navigate("/b");
1252+
expect(router.state.location.pathname).toBe("/b");
1253+
expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
1254+
expect(router.state.errors).toBeNull();
1255+
// Called again
1256+
expect(count).toBe(2);
1257+
// But not patched again
1258+
expect(router.routes).toMatchInlineSnapshot(`
1259+
[
12941260
{
1295-
path: "/",
1261+
"children": undefined,
1262+
"hasErrorBoundary": false,
1263+
"id": "0",
1264+
"path": "/",
12961265
},
12971266
{
1298-
id: "param",
1299-
path: ":param",
1267+
"children": undefined,
1268+
"hasErrorBoundary": false,
1269+
"id": "param",
1270+
"path": ":param",
13001271
},
1301-
],
1302-
async patchRoutesOnNavigation() {
1303-
count++;
1304-
// Nothing to patch - there is no better static route in this case
1305-
},
1306-
});
1307-
1308-
// Fill it up with 1000 paths
1309-
for (let i = 1; i <= 1000; i++) {
1310-
await router.navigate(`/path-${i}`);
1311-
expect(count).toBe(i);
1312-
expect(router.state.location.pathname).toBe(`/path-${i}`);
1313-
1314-
await router.navigate("/");
1315-
expect(count).toBe(i);
1316-
expect(router.state.location.pathname).toBe("/");
1317-
}
1318-
1319-
// Don't call patchRoutesOnNavigation since this is the first item in the queue
1320-
await router.navigate(`/path-1`);
1321-
expect(count).toBe(1000);
1322-
expect(router.state.location.pathname).toBe(`/path-1`);
1323-
1324-
// Call patchRoutesOnNavigation and evict the first item
1325-
await router.navigate(`/path-1001`);
1326-
expect(count).toBe(1001);
1327-
expect(router.state.location.pathname).toBe(`/path-1001`);
1328-
1329-
// Call patchRoutesOnNavigation since this item was evicted
1330-
await router.navigate(`/path-1`);
1331-
expect(count).toBe(1002);
1332-
expect(router.state.location.pathname).toBe(`/path-1`);
1272+
]
1273+
`);
13331274
});
13341275

13351276
describe("errors", () => {

packages/react-router/lib/router/router.ts

+27-38
Original file line numberDiff line numberDiff line change
@@ -819,10 +819,6 @@ export function createRouter(init: RouterInit): Router {
819819
let unlistenHistory: (() => void) | null = null;
820820
// Externally-provided functions to call on all state changes
821821
let subscribers = new Set<RouterSubscriber>();
822-
// FIFO queue of previously discovered routes to prevent re-calling on
823-
// subsequent navigations to the same path
824-
let discoveredRoutesMaxSize = 1000;
825-
let discoveredRoutes = new Set<string>();
826822
// Externally-provided object to hold scroll restoration locations during routing
827823
let savedScrollPositions: Record<string, number> | null = null;
828824
// Externally-provided function to get scroll restoration keys
@@ -3146,13 +3142,6 @@ export function createRouter(init: RouterInit): Router {
31463142
pathname: string
31473143
): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
31483144
if (patchRoutesOnNavigationImpl) {
3149-
// Don't bother re-calling patchRouteOnMiss for a path we've already
3150-
// processed. the last execution would have patched the route tree
3151-
// accordingly so `matches` here are already accurate.
3152-
if (discoveredRoutes.has(pathname)) {
3153-
return { active: false, matches };
3154-
}
3155-
31563145
if (!matches) {
31573146
let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>(
31583147
routesToUse,
@@ -3236,7 +3225,6 @@ export function createRouter(init: RouterInit): Router {
32363225

32373226
let newMatches = matchRoutes(routesToUse, pathname, basename);
32383227
if (newMatches) {
3239-
addToFifoQueue(pathname, discoveredRoutes);
32403228
return { type: "success", matches: newMatches };
32413229
}
32423230

@@ -3255,22 +3243,13 @@ export function createRouter(init: RouterInit): Router {
32553243
(m, i) => m.route.id === newPartialMatches![i].route.id
32563244
))
32573245
) {
3258-
addToFifoQueue(pathname, discoveredRoutes);
32593246
return { type: "success", matches: null };
32603247
}
32613248

32623249
partialMatches = newPartialMatches;
32633250
}
32643251
}
32653252

3266-
function addToFifoQueue(path: string, queue: Set<string>) {
3267-
if (queue.size >= discoveredRoutesMaxSize) {
3268-
let first = queue.values().next().value;
3269-
queue.delete(first);
3270-
}
3271-
queue.add(path);
3272-
}
3273-
32743253
function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
32753254
manifest = {};
32763255
inFlightDataRoutes = convertRoutesToDataRoutes(
@@ -4498,32 +4477,42 @@ function patchRoutesImpl(
44984477
manifest: RouteManifest,
44994478
mapRouteProperties: MapRoutePropertiesFunction
45004479
) {
4480+
let childrenToPatch: AgnosticDataRouteObject[];
45014481
if (routeId) {
45024482
let route = manifest[routeId];
45034483
invariant(
45044484
route,
45054485
`No route found to patch children into: routeId = ${routeId}`
45064486
);
4507-
let dataChildren = convertRoutesToDataRoutes(
4508-
children,
4509-
mapRouteProperties,
4510-
[routeId, "patch", String(route.children?.length || "0")],
4511-
manifest
4512-
);
4513-
if (route.children) {
4514-
route.children.push(...dataChildren);
4515-
} else {
4516-
route.children = dataChildren;
4487+
if (!route.children) {
4488+
route.children = [];
45174489
}
4490+
childrenToPatch = route.children;
45184491
} else {
4519-
let dataChildren = convertRoutesToDataRoutes(
4520-
children,
4521-
mapRouteProperties,
4522-
["patch", String(routesToUse.length || "0")],
4523-
manifest
4524-
);
4525-
routesToUse.push(...dataChildren);
4492+
childrenToPatch = routesToUse;
45264493
}
4494+
4495+
// Don't patch in routes we already know about so that `patch` is idempotent
4496+
// to simplify user-land code. This is useful because we re-call the
4497+
// `patchRoutesOnNavigation` function for matched routes with params.
4498+
let uniqueChildren = children.filter(
4499+
(a) =>
4500+
!childrenToPatch.some(
4501+
(b) =>
4502+
a.index === b.index &&
4503+
a.path === b.path &&
4504+
a.caseSensitive === b.caseSensitive
4505+
)
4506+
);
4507+
4508+
let newRoutes = convertRoutesToDataRoutes(
4509+
uniqueChildren,
4510+
mapRouteProperties,
4511+
[routeId || "_", "patch", String(childrenToPatch?.length || "0")],
4512+
manifest
4513+
);
4514+
4515+
childrenToPatch.push(...newRoutes);
45274516
}
45284517

45294518
/**

0 commit comments

Comments
 (0)