Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger a new router.routes identity during fog of war route patching #11740

Merged
merged 10 commits into from
Jun 27, 2024
19 changes: 19 additions & 0 deletions .changeset/lovely-apples-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@remix-run/router": patch
---

Trigger a new `router.routes` identity/reflow during fog of war route patching

- This also adds a new batched API for `router.patchRoutes` so you can perform multiple patches but only a single reflow at the end:

```js
// Apply one patch and reflow
router.patchRoutes(parentId, children);

// Apply multiples patches and a single reflow
router.patchRoutes((patch) => {
patch(parentId, children);
patch(parentId2, children2);
patch(parentId3, children3);
});
```
131 changes: 131 additions & 0 deletions packages/router/__tests__/lazy-discovery-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,137 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});

it("creates a new router.routes identity when patching routes", async () => {
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();

router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
},
{
id: "parent",
path: "parent",
},
],
async unstable_patchRoutesOnMiss({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
});
let originalRoutes = router.routes;

router.navigate("/parent/child");
childrenDfd.resolve([
{
id: "child",
path: "child",
},
]);
await tick();

expect(router.state.location.pathname).toBe("/parent/child");
expect(router.state.matches.map((m) => m.route.id)).toEqual([
"parent",
"child",
]);

expect(router.routes).not.toBe(originalRoutes);
});

it("allows patching externally/eagerly and triggers a reflow", async () => {
router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
},
{
id: "parent",
path: "parent",
},
],
});
let spy = jest.fn();
let unsubscribe = router.subscribe(spy);
let originalRoutes = router.routes;
router.patchRoutes("parent", [
{
id: "child",
path: "child",
},
]);
router.patchRoutes("parent", [
{
id: "child2",
path: "child2",
},
]);
expect(spy).toHaveBeenCalledTimes(2);
expect(router.routes).not.toBe(originalRoutes);

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

unsubscribe();
});

it("allows patching externally/eagerly and triggers a reflow (batch API)", async () => {
router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: "/",
},
{
id: "parent",
path: "parent",
},
],
});
let spy = jest.fn();
let unsubscribe = router.subscribe(spy);
let originalRoutes = router.routes;
router.patchRoutes((patch) => {
patch("parent", [
{
id: "child",
path: "child",
},
]);
patch("parent", [
{
id: "child2",
path: "child2",
},
]);
});

expect(spy).toHaveBeenCalledTimes(1);
expect(router.routes).not.toBe(originalRoutes);

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

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

unsubscribe();
});

describe("errors", () => {
it("lazy 404s (GET navigation)", async () => {
let childrenDfd = createDeferred<AgnosticDataRouteObject[]>();
Expand Down
87 changes: 70 additions & 17 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ export interface Router {
* @param routeId The parent route id
* @param children The additional children routes
*/
patchRoutes(
cb: (routeId: string | null, children: AgnosticRouteObject[]) => void
): void;
patchRoutes(routeId: string | null, children: AgnosticRouteObject[]): void;

/**
Expand Down Expand Up @@ -1222,6 +1225,7 @@ export function createRouter(init: RouterInit): Router {
isMutationMethod(state.navigation.formMethod) &&
location.state?._isRedirect !== true);

// Commit any in-flight routes at the end of the HMR revalidation "navigation"
if (inFlightDataRoutes) {
dataRoutes = inFlightDataRoutes;
inFlightDataRoutes = undefined;
Expand Down Expand Up @@ -3204,26 +3208,37 @@ export function createRouter(init: RouterInit): Router {
? partialMatches[partialMatches.length - 1].route
: null;
while (true) {
let isNonHMR = inFlightDataRoutes == null;
let routesToUse = inFlightDataRoutes || dataRoutes;
try {
await loadLazyRouteChildren(
patchRoutesOnMissImpl!,
pathname,
partialMatches,
dataRoutes || inFlightDataRoutes,
routesToUse,
manifest,
mapRouteProperties,
pendingPatchRoutes,
signal
);
} catch (e) {
return { type: "error", error: e, partialMatches };
} finally {
// If we are not in the middle of an HMR revalidation and we changed the
// routes, provide a new identity so when we `updateState` at the end of
// this navigation/fetch `router.routes` will be a new identity and
// trigger a re-run of memoized `router.routes` dependencies.
// HMR will already update the identity and reflow when it lands
// `inFlightDataRoutes` in `completeNavigation`
if (isNonHMR) {
dataRoutes = [...dataRoutes];
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update router.routes identity during normal navigation discovery via unstable_patchRoutesOnMiss

}
}

if (signal.aborted) {
return { type: "aborted" };
}

let routesToUse = inFlightDataRoutes || dataRoutes;
let newMatches = matchRoutes(routesToUse, pathname, basename);
let matchedSplat = false;
if (newMatches) {
Expand Down Expand Up @@ -3284,6 +3299,52 @@ export function createRouter(init: RouterInit): Router {
);
}

// Standard implementation - patch children into a parent and `updateState`
function patchRoutes(
routeId: string,
children: AgnosticDataRouteObject[]
): void;
// Batch implementation - perform multiple patches followed by a single `updateState`
function patchRoutes(
cb: (
patch: (routeId: string, children: AgnosticDataRouteObject[]) => void
) => void
): void;
function patchRoutes(
routeId:
| string
| ((
patch: (routeId: string, children: AgnosticDataRouteObject[]) => void
) => void),
children?: AgnosticDataRouteObject[]
): void {
let isNonHMR = inFlightDataRoutes == null;
let routesToUse = inFlightDataRoutes || dataRoutes;
if (typeof routeId === "function") {
routeId((r, c) =>
patchRoutesImpl(r, c, routesToUse, manifest, mapRouteProperties)
);
brophdawg11 marked this conversation as resolved.
Show resolved Hide resolved
} else if (typeof routeId === "string" && children != null) {
patchRoutesImpl(
routeId,
children,
routesToUse,
manifest,
mapRouteProperties
);
}

// If we are not in the middle of an HMR revalidation and we changed the
// routes, provide a new identity and trigger a reflow via `updateState`
// to re-run memoized `router.routes` dependencies.
// HMR will already update the identity and reflow when it lands
// `inFlightDataRoutes` in `completeNavigation`
if (isNonHMR) {
dataRoutes = [...dataRoutes];
updateState({});
}
Comment on lines +3319 to +3322
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update router.routes identity and reflow during eager router.patchRoutes flows

}

router = {
get basename() {
return basename;
Expand Down Expand Up @@ -3315,15 +3376,7 @@ export function createRouter(init: RouterInit): Router {
dispose,
getBlocker,
deleteBlocker,
patchRoutes(routeId, children) {
return patchRoutes(
routeId,
children,
dataRoutes || inFlightDataRoutes,
manifest,
mapRouteProperties
);
},
patchRoutes,
_internalFetchControllers: fetchControllers,
_internalActiveDeferreds: activeDeferreds,
// TODO: Remove setRoutes, it's temporary to avoid dealing with
Expand Down Expand Up @@ -4488,7 +4541,7 @@ function shouldRevalidateLoader(
}

/**
* Idempotent utility to execute route.children() method to lazily load route
* Idempotent utility to execute patchRoutesOnMiss() to lazily load route
* definitions and update the routes/routeManifest
*/
async function loadLazyRouteChildren(
Expand All @@ -4510,7 +4563,7 @@ async function loadLazyRouteChildren(
matches,
patch: (routeId, children) => {
if (!signal.aborted) {
patchRoutes(
patchRoutesImpl(
routeId,
children,
routes,
Expand All @@ -4531,10 +4584,10 @@ async function loadLazyRouteChildren(
}
}

function patchRoutes(
function patchRoutesImpl(
routeId: string | null,
children: AgnosticRouteObject[],
routes: AgnosticDataRouteObject[],
routesToUse: AgnosticDataRouteObject[],
manifest: RouteManifest,
mapRouteProperties: MapRoutePropertiesFunction
) {
Expand All @@ -4559,10 +4612,10 @@ function patchRoutes(
let dataChildren = convertRoutesToDataRoutes(
children,
mapRouteProperties,
["patch", String(routes.length || "0")],
["patch", String(routesToUse.length || "0")],
manifest
);
routes.push(...dataChildren);
routesToUse.push(...dataChildren);
}
}

Expand Down
Loading