diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3b4451730d..25832711c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,173 +13,175 @@ We manage release notes in this file instead of the paginated Github Releases Pa
Table of Contents
- [React Router Releases](#react-router-releases)
+ - [v6.26.1](#v6261)
+ - [Patch Changes](#patch-changes)
- [v6.26.0](#v6260)
- [Minor Changes](#minor-changes)
- - [Patch Changes](#patch-changes)
- - [v6.25.1](#v6251)
- [Patch Changes](#patch-changes-1)
+ - [v6.25.1](#v6251)
+ - [Patch Changes](#patch-changes-2)
- [v6.25.0](#v6250)
- [What's Changed](#whats-changed)
- [Stabilized `v7_skipActionErrorRevalidation`](#stabilized-v7_skipactionerrorrevalidation)
- [Minor Changes](#minor-changes-1)
- - [Patch Changes](#patch-changes-2)
- - [v6.24.1](#v6241)
- [Patch Changes](#patch-changes-3)
+ - [v6.24.1](#v6241)
+ - [Patch Changes](#patch-changes-4)
- [v6.24.0](#v6240)
- [What's Changed](#whats-changed-1)
- [Lazy Route Discovery (a.k.a. "Fog of War")](#lazy-route-discovery-aka-fog-of-war)
- [Minor Changes](#minor-changes-2)
- - [Patch Changes](#patch-changes-4)
- - [v6.23.1](#v6231)
- [Patch Changes](#patch-changes-5)
+ - [v6.23.1](#v6231)
+ - [Patch Changes](#patch-changes-6)
- [v6.23.0](#v6230)
- [What's Changed](#whats-changed-2)
- [Data Strategy (unstable)](#data-strategy-unstable)
- [Skip Action Error Revalidation (unstable)](#skip-action-error-revalidation-unstable)
- [Minor Changes](#minor-changes-3)
- [v6.22.3](#v6223)
- - [Patch Changes](#patch-changes-6)
- - [v6.22.2](#v6222)
- [Patch Changes](#patch-changes-7)
- - [v6.22.1](#v6221)
+ - [v6.22.2](#v6222)
- [Patch Changes](#patch-changes-8)
+ - [v6.22.1](#v6221)
+ - [Patch Changes](#patch-changes-9)
- [v6.22.0](#v6220)
- [What's Changed](#whats-changed-3)
- [Core Web Vitals Technology Report Flag](#core-web-vitals-technology-report-flag)
- [Minor Changes](#minor-changes-4)
- - [Patch Changes](#patch-changes-9)
- - [v6.21.3](#v6213)
- [Patch Changes](#patch-changes-10)
- - [v6.21.2](#v6212)
+ - [v6.21.3](#v6213)
- [Patch Changes](#patch-changes-11)
- - [v6.21.1](#v6211)
+ - [v6.21.2](#v6212)
- [Patch Changes](#patch-changes-12)
+ - [v6.21.1](#v6211)
+ - [Patch Changes](#patch-changes-13)
- [v6.21.0](#v6210)
- [What's Changed](#whats-changed-4)
- [`future.v7_relativeSplatPath`](#futurev7_relativesplatpath)
- [Partial Hydration](#partial-hydration)
- [Minor Changes](#minor-changes-5)
- - [Patch Changes](#patch-changes-13)
- - [v6.20.1](#v6201)
- [Patch Changes](#patch-changes-14)
+ - [v6.20.1](#v6201)
+ - [Patch Changes](#patch-changes-15)
- [v6.20.0](#v6200)
- [Minor Changes](#minor-changes-6)
- - [Patch Changes](#patch-changes-15)
+ - [Patch Changes](#patch-changes-16)
- [v6.19.0](#v6190)
- [What's Changed](#whats-changed-5)
- [`unstable_flushSync` API](#unstable_flushsync-api)
- [Minor Changes](#minor-changes-7)
- - [Patch Changes](#patch-changes-16)
+ - [Patch Changes](#patch-changes-17)
- [v6.18.0](#v6180)
- [What's Changed](#whats-changed-6)
- [New Fetcher APIs](#new-fetcher-apis)
- [Persistence Future Flag (`future.v7_fetcherPersist`)](#persistence-future-flag-futurev7_fetcherpersist)
- [Minor Changes](#minor-changes-8)
- - [Patch Changes](#patch-changes-17)
+ - [Patch Changes](#patch-changes-18)
- [v6.17.0](#v6170)
- [What's Changed](#whats-changed-7)
- [View Transitions 🚀](#view-transitions-)
- [Minor Changes](#minor-changes-9)
- - [Patch Changes](#patch-changes-18)
+ - [Patch Changes](#patch-changes-19)
- [v6.16.0](#v6160)
- [Minor Changes](#minor-changes-10)
- - [Patch Changes](#patch-changes-19)
+ - [Patch Changes](#patch-changes-20)
- [v6.15.0](#v6150)
- [Minor Changes](#minor-changes-11)
- - [Patch Changes](#patch-changes-20)
- - [v6.14.2](#v6142)
- [Patch Changes](#patch-changes-21)
- - [v6.14.1](#v6141)
+ - [v6.14.2](#v6142)
- [Patch Changes](#patch-changes-22)
+ - [v6.14.1](#v6141)
+ - [Patch Changes](#patch-changes-23)
- [v6.14.0](#v6140)
- [What's Changed](#whats-changed-8)
- [JSON/Text Submissions](#jsontext-submissions)
- [Minor Changes](#minor-changes-12)
- - [Patch Changes](#patch-changes-23)
+ - [Patch Changes](#patch-changes-24)
- [v6.13.0](#v6130)
- [What's Changed](#whats-changed-9)
- [`future.v7_startTransition`](#futurev7_starttransition)
- [Minor Changes](#minor-changes-13)
- - [Patch Changes](#patch-changes-24)
- - [v6.12.1](#v6121)
- [Patch Changes](#patch-changes-25)
+ - [v6.12.1](#v6121)
+ - [Patch Changes](#patch-changes-26)
- [v6.12.0](#v6120)
- [What's Changed](#whats-changed-10)
- [`React.startTransition` support](#reactstarttransition-support)
- [Minor Changes](#minor-changes-14)
- - [Patch Changes](#patch-changes-26)
- - [v6.11.2](#v6112)
- [Patch Changes](#patch-changes-27)
- - [v6.11.1](#v6111)
+ - [v6.11.2](#v6112)
- [Patch Changes](#patch-changes-28)
+ - [v6.11.1](#v6111)
+ - [Patch Changes](#patch-changes-29)
- [v6.11.0](#v6110)
- [Minor Changes](#minor-changes-15)
- - [Patch Changes](#patch-changes-29)
+ - [Patch Changes](#patch-changes-30)
- [v6.10.0](#v6100)
- [What's Changed](#whats-changed-11)
- [Minor Changes](#minor-changes-16)
- [`future.v7_normalizeFormMethod`](#futurev7_normalizeformmethod)
- - [Patch Changes](#patch-changes-30)
+ - [Patch Changes](#patch-changes-31)
- [v6.9.0](#v690)
- [What's Changed](#whats-changed-12)
- [`Component`/`ErrorBoundary` route properties](#componenterrorboundary-route-properties)
- [Introducing Lazy Route Modules](#introducing-lazy-route-modules)
- [Minor Changes](#minor-changes-17)
- - [Patch Changes](#patch-changes-31)
- - [v6.8.2](#v682)
- [Patch Changes](#patch-changes-32)
- - [v6.8.1](#v681)
+ - [v6.8.2](#v682)
- [Patch Changes](#patch-changes-33)
+ - [v6.8.1](#v681)
+ - [Patch Changes](#patch-changes-34)
- [v6.8.0](#v680)
- [Minor Changes](#minor-changes-18)
- - [Patch Changes](#patch-changes-34)
+ - [Patch Changes](#patch-changes-35)
- [v6.7.0](#v670)
- [Minor Changes](#minor-changes-19)
- - [Patch Changes](#patch-changes-35)
- - [v6.6.2](#v662)
- [Patch Changes](#patch-changes-36)
- - [v6.6.1](#v661)
+ - [v6.6.2](#v662)
- [Patch Changes](#patch-changes-37)
+ - [v6.6.1](#v661)
+ - [Patch Changes](#patch-changes-38)
- [v6.6.0](#v660)
- [What's Changed](#whats-changed-13)
- [Minor Changes](#minor-changes-20)
- - [Patch Changes](#patch-changes-38)
+ - [Patch Changes](#patch-changes-39)
- [v6.5.0](#v650)
- [What's Changed](#whats-changed-14)
- [Minor Changes](#minor-changes-21)
- - [Patch Changes](#patch-changes-39)
- - [v6.4.5](#v645)
- [Patch Changes](#patch-changes-40)
- - [v6.4.4](#v644)
+ - [v6.4.5](#v645)
- [Patch Changes](#patch-changes-41)
- - [v6.4.3](#v643)
+ - [v6.4.4](#v644)
- [Patch Changes](#patch-changes-42)
- - [v6.4.2](#v642)
+ - [v6.4.3](#v643)
- [Patch Changes](#patch-changes-43)
- - [v6.4.1](#v641)
+ - [v6.4.2](#v642)
- [Patch Changes](#patch-changes-44)
+ - [v6.4.1](#v641)
+ - [Patch Changes](#patch-changes-45)
- [v6.4.0](#v640)
- [What's Changed](#whats-changed-15)
- [Remix Data APIs](#remix-data-apis)
- - [Patch Changes](#patch-changes-45)
+ - [Patch Changes](#patch-changes-46)
- [v6.3.0](#v630)
- [Minor Changes](#minor-changes-22)
- [v6.2.2](#v622)
- - [Patch Changes](#patch-changes-46)
- - [v6.2.1](#v621)
- [Patch Changes](#patch-changes-47)
+ - [v6.2.1](#v621)
+ - [Patch Changes](#patch-changes-48)
- [v6.2.0](#v620)
- [Minor Changes](#minor-changes-23)
- - [Patch Changes](#patch-changes-48)
- - [v6.1.1](#v611)
- [Patch Changes](#patch-changes-49)
+ - [v6.1.1](#v611)
+ - [Patch Changes](#patch-changes-50)
- [v6.1.0](#v610)
- [Minor Changes](#minor-changes-24)
- - [Patch Changes](#patch-changes-50)
- - [v6.0.2](#v602)
- [Patch Changes](#patch-changes-51)
- - [v6.0.1](#v601)
+ - [v6.0.2](#v602)
- [Patch Changes](#patch-changes-52)
+ - [v6.0.1](#v601)
+ - [Patch Changes](#patch-changes-53)
- [v6.0.0](#v600)
@@ -203,6 +205,18 @@ Date: YYYY-MM-DD
**Full Changelog**: [`v6.X.Y...v6.X.Y`](https://github.com/remix-run/react-router/compare/react-router@6.X.Y...react-router@6.X.Y)
-->
+## v6.26.1
+
+Date: 2024-08-15
+
+### Patch Changes
+
+- Rename `unstable_patchRoutesOnMiss` to `unstable_patchRoutesOnNavigation` to match new behavior ([#11888](https://github.com/remix-run/react-router/pull/11888))
+- Update `unstable_patchRoutesOnNavigation` 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 ([#11883](https://github.com/remix-run/react-router/pull/11883))
+ - We also now leverage an internal FIFO queue of previous paths we've already called `unstable_patchRoutesOnNavigation` against so that we don't re-call on subsequent navigations to the same path
+
+**Full Changelog**: [`v6.26.0...v6.26.1`](https://github.com/remix-run/react-router/compare/react-router@6.26.0...react-router@6.26.1)
+
## v6.26.0
Date: 2024-08-01
diff --git a/integration/fog-of-war-test.ts b/integration/fog-of-war-test.ts
index 4e171e50fd..652e4f8e61 100644
--- a/integration/fog-of-war-test.ts
+++ b/integration/fog-of-war-test.ts
@@ -692,6 +692,7 @@ test.describe("Fog of War", () => {
"routes/parent._a._b",
"routes/parent._a._b._index",
]);
+ expect(manifestRequests).toEqual([]);
// Without pre-loading the index, we'd "match" `/parent` to just the
// parent route client side and never fetch the children pathless/index routes
@@ -715,6 +716,444 @@ test.describe("Fog of War", () => {
]);
});
+ test("detects higher-ranking static routes on the server when a slug match is already known by the client", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Slug
;
+ Go to /static
+ >
+ );
+ }
+ `,
+ "app/routes/static.tsx": js`
+ export default function Component() {
+ return Static
;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index", "routes/$slug"]);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fsomething/),
+ ]);
+ manifestRequests = [];
+
+ await app.clickLink("/something");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug");
+ expect(manifestRequests).toEqual([]);
+
+ // This will require a new fetch for the /static route
+ await app.clickLink("/static");
+ await page.waitForSelector("#static");
+ expect(await app.getHtml("#static")).toMatch("Static");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fstatic/),
+ ]);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index", "routes/$slug", "routes/static"]);
+ });
+
+ test("detects higher-ranking static routes on the server when a splat match is already known by the client", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
+ }
+ `,
+ "app/routes/$.tsx": js`
+ import { Link } from "react-router";
+ export default function Component() {
+ return (
+ <>
+ Splat
;
+ Go to /static
+ >
+ );
+ }
+ `,
+ "app/routes/static.tsx": js`
+ export default function Component() {
+ return Static
;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index", "routes/$"]);
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fsomething/),
+ ]);
+ manifestRequests = [];
+
+ await app.clickLink("/something");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat");
+ expect(manifestRequests).toEqual([]);
+
+ // This will require a new fetch for the /static route
+ await app.clickLink("/static");
+ await page.waitForSelector("#static");
+ expect(await app.getHtml("#static")).toMatch("Static");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fstatic/),
+ ]);
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index", "routes/$", "routes/static"]);
+ });
+
+ test("does not re-request for previously discovered slug routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
;
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Slug: {params.slug}
;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click /a which will discover via a manifest request
+ await app.clickLink("/a");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: a");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fa/),
+ ]);
+ manifestRequests = [];
+
+ // Go back home
+ await app.clickLink("/");
+ await page.waitForSelector("#index");
+ expect(manifestRequests).toEqual([]);
+
+ // Click /a again which will not re-discover
+ await app.clickLink("/a");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: a");
+ expect(manifestRequests).toEqual([]);
+ manifestRequests = [];
+
+ // Click /b which will need to discover
+ await app.clickLink("/b");
+ await page.waitForSelector("#slug");
+ expect(await app.getHtml("#slug")).toMatch("Slug: b");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fb/),
+ ]);
+ });
+
+ test("does not re-request for previously discovered splat routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
;
+ }
+ `,
+ "app/routes/$.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Splat: {params["*"]}
;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click /a which will discover via a manifest request
+ await app.clickLink("/a");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: a");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fa/),
+ ]);
+ manifestRequests = [];
+
+ // Go back home
+ await app.clickLink("/");
+ await page.waitForSelector("#index");
+ expect(manifestRequests).toEqual([]);
+
+ // Click /a again which will not re-discover
+ await app.clickLink("/a");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: a");
+ expect(manifestRequests).toEqual([]);
+ manifestRequests = [];
+
+ // Click /b which will need to discover
+ await app.clickLink("/b/c");
+ await page.waitForSelector("#splat");
+ expect(await app.getHtml("#splat")).toMatch("Splat: b/c");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fb%2Fc/),
+ ]);
+ });
+
+ test("does not re-request for previously navigated 404 routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/root.tsx": js`
+ import { Link, Outlet, Scripts } from "react-router";
+ export function Layout({ children }) {
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+ }
+ export default function Root() {
+ return ;
+ }
+ export function ErrorBoundary() {
+ return Error
;
+ }
+ `,
+
+ "app/routes/_index.tsx": js`
+ export default function Index() {
+ return Index
;
+ }
+ `,
+ "app/routes/$slug.tsx": js`
+ import { Link, useParams } from "react-router";
+ export default function Component() {
+ let params = useParams();
+ return Slug: {params.slug}
;
+ }
+ `,
+ },
+ });
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ let manifestRequests: string[] = [];
+ page.on("request", (req) => {
+ if (req.url().includes("/__manifest")) {
+ manifestRequests.push(req.url());
+ }
+ });
+
+ await app.goto("/", true);
+ expect(await app.getHtml("#index")).toMatch("Index");
+ expect(
+ await page.evaluate(() =>
+ Object.keys((window as any).__remixManifest.routes)
+ )
+ ).toEqual(["root", "routes/_index"]);
+ expect(manifestRequests.length).toBe(0);
+
+ // Click a 404 link which will try to discover via a manifest request
+ await app.clickLink("/not/a/path");
+ await page.waitForSelector("#error");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(
+ /\/__manifest\?version=[a-z0-9]{8}&p=%2Fnot%2Fa%2Fpath/
+ ),
+ ]);
+ manifestRequests = [];
+
+ // Go to a valid slug route
+ await app.clickLink("/something");
+ await page.waitForSelector("#slug");
+ expect(manifestRequests).toEqual([
+ expect.stringMatching(/\/__manifest\?version=[a-z0-9]{8}&p=%2Fsomething/),
+ ]);
+ manifestRequests = [];
+
+ // Click the same 404 link again which will not re-discover
+ await app.clickLink("/not/a/path");
+ await page.waitForSelector("#error");
+ expect(manifestRequests).toEqual([]);
+ });
+
test("skips prefetching if the URL gets too large", async ({ page }) => {
let fixture = await createFixture({
files: {
diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index a2ef2570f1..f219bb543e 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -1350,6 +1350,67 @@ test.describe("single-fetch", () => {
expect(await app.getHtml("#target")).toContain("Target");
});
+ test("processes redirects when a basename is present", async ({ page }) => {
+ let fixture = await createFixture({
+ files: {
+ ...files,
+ "vite.config.ts": js`
+ import { defineConfig } from "vite";
+ import { vitePlugin as remix } from "@react-router/dev";
+ export default defineConfig({
+ plugins: [
+ remix({
+ basename: '/base',
+ }),
+ ],
+ });
+ `,
+ "app/routes/data.tsx": js`
+ import { redirect } from 'react-router';
+ export function loader() {
+ throw redirect('/target');
+ }
+ export default function Component() {
+ return null
+ }
+ `,
+ "app/routes/target.tsx": js`
+ export default function Component() {
+ return Target
+ }
+ `,
+ },
+ });
+
+ console.error = () => {};
+
+ let res = await fixture.requestDocument("/base/data");
+ expect(res.status).toBe(302);
+ expect(res.headers.get("Location")).toBe("/base/target");
+ expect(await res.text()).toBe("");
+
+ let { status, data } = await fixture.requestSingleFetchData(
+ "/base/data.data"
+ );
+ expect(data).toEqual({
+ [SingleFetchRedirectSymbol]: {
+ status: 302,
+ redirect: "/target",
+ reload: false,
+ replace: false,
+ revalidate: false,
+ },
+ });
+ expect(status).toBe(202);
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/base/");
+ await app.clickLink("/base/data");
+ await page.waitForSelector("#target");
+ expect(await app.getHtml("#target")).toContain("Target");
+ });
+
test("processes thrown loader errors", async ({ page }) => {
let fixture = await createFixture({
files: {
diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md
index 9c4b1fa19a..dbde50bfec 100644
--- a/packages/react-router/CHANGELOG.md
+++ b/packages/react-router/CHANGELOG.md
@@ -1,5 +1,13 @@
# `react-router`
+## 6.26.1
+
+### Patch Changes
+
+- Rename `unstable_patchRoutesOnMiss` to `unstable_patchRoutesOnNavigation` to match new behavior ([#11888](https://github.com/remix-run/react-router/pull/11888))
+- Updated dependencies:
+ - `@remix-run/router@1.19.1`
+
## 6.26.0
### Minor Changes
@@ -15,9 +23,7 @@
## 6.25.1
-### Patch Changes
-
-- Memoize some `RouterProvider` internals to reduce unnecessary re-renders ([#11803](https://github.com/remix-run/react-router/pull/11803))
+No significant changes to this package were made in this release. [See the repo `CHANGELOG.md`](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) for an overview of all changes in v6.25.1.
## 6.25.0
diff --git a/packages/react-router/__tests__/dom/partial-hydration-test.tsx b/packages/react-router/__tests__/dom/partial-hydration-test.tsx
index f3fb697ae3..dbd6926f20 100644
--- a/packages/react-router/__tests__/dom/partial-hydration-test.tsx
+++ b/packages/react-router/__tests__/dom/partial-hydration-test.tsx
@@ -32,7 +32,7 @@ describe("Partial Hydration Behavior", () => {
testPartialHydration(createMemoryRouter, ReactRouter_RouterProvider);
// these tests only run for memory since we just need to set initialEntries
- it("supports partial hydration w/patchRoutesOnMiss (leaf fallback)", async () => {
+ it("supports partial hydration w/patchRoutesOnNavigation (leaf fallback)", async () => {
let parentDfd = createDeferred();
let childDfd = createDeferred();
let router = createMemoryRouter(
@@ -70,7 +70,7 @@ describe("Partial Hydration Behavior", () => {
future: {
v7_partialHydration: true,
},
- unstable_patchRoutesOnMiss({ path, patch }) {
+ unstable_patchRoutesOnNavigation({ path, patch }) {
if (path === "/parent/child") {
patch("parent", [
{
@@ -120,7 +120,7 @@ describe("Partial Hydration Behavior", () => {
`);
});
- it("supports partial hydration w/patchRoutesOnMiss (root fallback)", async () => {
+ it("supports partial hydration w/patchRoutesOnNavigation (root fallback)", async () => {
let parentDfd = createDeferred();
let childDfd = createDeferred();
let router = createMemoryRouter(
@@ -158,7 +158,7 @@ describe("Partial Hydration Behavior", () => {
future: {
v7_partialHydration: true,
},
- unstable_patchRoutesOnMiss({ path, patch }) {
+ unstable_patchRoutesOnNavigation({ path, patch }) {
if (path === "/parent/child") {
patch("parent", [
{
diff --git a/packages/react-router/__tests__/router/lazy-discovery-test.ts b/packages/react-router/__tests__/router/lazy-discovery-test.ts
index 6a1c1b0da7..03e45e9092 100644
--- a/packages/react-router/__tests__/router/lazy-discovery-test.ts
+++ b/packages/react-router/__tests__/router/lazy-discovery-test.ts
@@ -36,7 +36,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
loader: () => loaderDfd.promise,
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -92,7 +92,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ patch, matches }) {
+ async unstable_patchRoutesOnNavigation({ patch, matches }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -148,7 +148,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
loader: () => loaderDfd.promise,
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -222,7 +222,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ patch, matches }) {
+ async unstable_patchRoutesOnNavigation({ patch, matches }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -286,7 +286,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ path, matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ path, matches, patch }) {
let routeId = last(matches).route.id;
calls.push([path, routeId]);
patch("a", await aDfd.promise);
@@ -341,7 +341,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ path, matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ path, matches, patch }) {
let routeId = last(matches).route.id;
if (!path) {
return;
@@ -444,7 +444,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
},
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
let leafRoute = last(matches).route;
patch(leafRoute.id, await leafRoute.handle.loadChildren?.());
},
@@ -474,7 +474,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -510,6 +510,121 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});
+ it("de-prioritizes dynamic param routes in favor of looking for better async matches", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "slug",
+ path: "/:slug",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ patch }) {
+ await tick();
+ patch(null, [
+ {
+ id: "static",
+ path: "/static",
+ },
+ ]);
+ },
+ });
+
+ await router.navigate("/static");
+ expect(router.state.location.pathname).toBe("/static");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
+ });
+
+ it("de-prioritizes dynamic param routes in favor of looking for better async matches (product/:slug)", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "slug",
+ path: "/product/:slug",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ patch }) {
+ await tick();
+ patch(null, [
+ {
+ id: "static",
+ path: "/product/static",
+ },
+ ]);
+ },
+ });
+
+ await router.navigate("/product/static");
+ expect(router.state.location.pathname).toBe("/product/static");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
+ });
+
+ it("de-prioritizes dynamic param routes in favor of looking for better async matches (child route)", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "product",
+ path: "/product",
+ children: [
+ {
+ id: "slug",
+ path: ":slug",
+ },
+ ],
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ patch }) {
+ await tick();
+ patch("product", [
+ {
+ id: "static",
+ path: "static",
+ },
+ ]);
+ },
+ });
+
+ await router.navigate("/product/static");
+ expect(router.state.location.pathname).toBe("/product/static");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual([
+ "product",
+ "static",
+ ]);
+ });
+
+ it("matches dynamic params when other paths don't pan out", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "slug",
+ path: "/:slug",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
+ await tick();
+ },
+ });
+
+ await router.navigate("/a");
+ expect(router.state.location.pathname).toBe("/a");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["slug"]);
+ });
+
it("de-prioritizes splat routes in favor of looking for better async matches", async () => {
router = createRouter({
history: createMemoryHistory(),
@@ -526,7 +641,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -556,7 +671,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "/splat/*",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
patch(null, [
{
@@ -572,6 +687,43 @@ describe("Lazy Route Discovery (Fog of War)", () => {
expect(router.state.matches.map((m) => m.route.id)).toEqual(["static"]);
});
+ it("de-prioritizes splat routes in favor of looking for better async matches (child route)", async () => {
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "product",
+ path: "/product",
+ children: [
+ {
+ id: "splat",
+ path: "*",
+ },
+ ],
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ patch }) {
+ await tick();
+ patch("product", [
+ {
+ id: "static",
+ path: "static",
+ },
+ ]);
+ },
+ });
+
+ await router.navigate("/product/static");
+ expect(router.state.location.pathname).toBe("/product/static");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual([
+ "product",
+ "static",
+ ]);
+ });
+
it("matches splats when other paths don't pan out", async () => {
router = createRouter({
history: createMemoryHistory(),
@@ -588,7 +740,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -606,6 +758,50 @@ describe("Lazy Route Discovery (Fog of War)", () => {
expect(router.state.matches.map((m) => m.route.id)).toEqual(["splat"]);
});
+ it("recurses unstable_patchRoutesOnNavigation until a match is found", async () => {
+ let count = 0;
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "a",
+ path: "a",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
+ await tick();
+ count++;
+ if (last(matches).route.id === "a") {
+ patch("a", [
+ {
+ id: "b",
+ path: "b",
+ },
+ ]);
+ } else if (last(matches).route.id === "b") {
+ patch("b", [
+ {
+ id: "c",
+ path: "c",
+ },
+ ]);
+ }
+ },
+ });
+
+ 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",
+ ]);
+ expect(count).toBe(2);
+ });
+
it("discovers routes during initial hydration", async () => {
let childrenDfd = createDeferred();
let loaderDfd = createDeferred();
@@ -623,7 +819,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
loader: () => loaderDfd.promise,
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -674,7 +870,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "*",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch(null, children);
},
@@ -712,7 +908,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
splat: "SPLAT 1",
},
},
- async unstable_patchRoutesOnMiss() {
+ async unstable_patchRoutesOnNavigation() {
throw new Error("Should not be called");
},
});
@@ -739,7 +935,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "/parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
patch(null, await childrenDfd.promise);
},
});
@@ -790,7 +986,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "/:param",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
// We matched for the param but we want to patch in under root
expect(matches.length).toBe(1);
expect(matches[0].route.id).toBe("param");
@@ -845,7 +1041,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "*",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
// We matched for the splat but we want to patch in at the top
expect(matches.length).toBe(1);
expect(matches[0].route.id).toBe("splat");
@@ -895,7 +1091,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "/nope",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
expect(matches.length).toBe(0);
let children = await childrenDfd.promise;
patch(null, children);
@@ -946,7 +1142,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -1006,6 +1202,136 @@ describe("Lazy Route Discovery (Fog of War)", () => {
unsubscribe();
});
+ it('does not re-call for previously called "good" paths', async () => {
+ let count = 0;
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "param",
+ path: ":param",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation() {
+ count++;
+ await tick();
+ // Nothing to patch - there is no better static route in this case
+ },
+ });
+
+ await router.navigate("/whatever");
+ expect(count).toBe(1);
+ expect(router.state.location.pathname).toBe("/whatever");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
+
+ await router.navigate("/");
+ expect(count).toBe(1);
+ expect(router.state.location.pathname).toBe("/");
+
+ await router.navigate("/whatever");
+ expect(count).toBe(1); // Not called again
+ expect(router.state.location.pathname).toBe("/whatever");
+ expect(router.state.matches.map((m) => m.route.id)).toEqual(["param"]);
+ });
+
+ it("does not re-call for previously called 404 paths", async () => {
+ let count = 0;
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ id: "index",
+ path: "/",
+ },
+ {
+ id: "static",
+ path: "static",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation() {
+ count++;
+ },
+ });
+
+ await router.navigate("/junk");
+ expect(count).toBe(1);
+ expect(router.state.location.pathname).toBe("/junk");
+ expect(router.state.errors?.index).toEqual(
+ new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ new Error('No route matches URL "/junk"'),
+ true
+ )
+ );
+
+ await router.navigate("/");
+ expect(count).toBe(1);
+ expect(router.state.location.pathname).toBe("/");
+ expect(router.state.errors).toBeNull();
+
+ await router.navigate("/junk");
+ expect(count).toBe(1);
+ expect(router.state.location.pathname).toBe("/junk");
+ expect(router.state.errors?.index).toEqual(
+ new ErrorResponseImpl(
+ 404,
+ "Not Found",
+ new Error('No route matches URL "/junk"'),
+ true
+ )
+ );
+ });
+
+ it("caps internal fifo queue at 1000 paths", async () => {
+ let count = 0;
+ router = createRouter({
+ history: createMemoryHistory(),
+ routes: [
+ {
+ path: "/",
+ },
+ {
+ id: "param",
+ path: ":param",
+ },
+ ],
+ async unstable_patchRoutesOnNavigation() {
+ count++;
+ // Nothing to patch - there is no better static route in this case
+ },
+ });
+
+ // Fill it up with 1000 paths
+ for (let i = 1; i <= 1000; i++) {
+ await router.navigate(`/path-${i}`);
+ expect(count).toBe(i);
+ expect(router.state.location.pathname).toBe(`/path-${i}`);
+
+ await router.navigate("/");
+ expect(count).toBe(i);
+ expect(router.state.location.pathname).toBe("/");
+ }
+
+ // Don't call patchRoutesOnNavigation since this is the first item in the queue
+ await router.navigate(`/path-1`);
+ expect(count).toBe(1000);
+ expect(router.state.location.pathname).toBe(`/path-1`);
+
+ // Call patchRoutesOnNavigation and evict the first item
+ await router.navigate(`/path-1001`);
+ expect(count).toBe(1001);
+ expect(router.state.location.pathname).toBe(`/path-1001`);
+
+ // Call patchRoutesOnNavigation since this item was evicted
+ await router.navigate(`/path-1`);
+ expect(count).toBe(1002);
+ expect(router.state.location.pathname).toBe(`/path-1`);
+ });
+
describe("errors", () => {
it("lazy 404s (GET navigation)", async () => {
let childrenDfd = createDeferred();
@@ -1021,7 +1347,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -1076,7 +1402,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -1133,7 +1459,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1186,7 +1512,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1239,7 +1565,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1291,7 +1617,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1348,7 +1674,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1405,7 +1731,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1449,7 +1775,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
]);
});
- it("handles errors thrown from patchRoutesOnMiss() (GET navigation)", async () => {
+ it("handles errors thrown from patchRoutesOnNavigation() (GET navigation)", async () => {
let shouldThrow = true;
router = createRouter({
history: createMemoryHistory(),
@@ -1463,7 +1789,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
await tick();
if (shouldThrow) {
shouldThrow = false;
@@ -1491,7 +1817,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
400,
"Bad Request",
new Error(
- 'Unable to match URL "/a/b" - the `unstable_patchRoutesOnMiss()` ' +
+ 'Unable to match URL "/a/b" - the `unstable_patchRoutesOnNavigation()` ' +
"function threw the following error:\nError: broke!"
),
true
@@ -1521,7 +1847,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
expect(router.state.matches.map((m) => m.route.id)).toEqual(["a", "b"]);
});
- it("handles errors thrown from patchRoutesOnMiss() (POST navigation)", async () => {
+ it("handles errors thrown from patchRoutesOnNavigation() (POST navigation)", async () => {
let shouldThrow = true;
router = createRouter({
history: createMemoryHistory(),
@@ -1535,7 +1861,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
await tick();
if (shouldThrow) {
shouldThrow = false;
@@ -1566,7 +1892,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
400,
"Bad Request",
new Error(
- 'Unable to match URL "/a/b" - the `unstable_patchRoutesOnMiss()` ' +
+ 'Unable to match URL "/a/b" - the `unstable_patchRoutesOnNavigation()` ' +
"function threw the following error:\nError: broke!"
),
true
@@ -1599,7 +1925,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
expect(router.state.matches.map((m) => m.route.id)).toEqual(["a", "b"]);
});
- it("bubbles errors thrown from patchRoutesOnMiss() during hydration", async () => {
+ it("bubbles errors thrown from patchRoutesOnNavigation() during hydration", async () => {
router = createRouter({
history: createMemoryHistory({
initialEntries: ["/parent/child/grandchild"],
@@ -1617,7 +1943,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
],
},
],
- async unstable_patchRoutesOnMiss() {
+ async unstable_patchRoutesOnNavigation() {
await tick();
throw new Error("broke!");
},
@@ -1644,7 +1970,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
"Bad Request",
new Error(
'Unable to match URL "/parent/child/grandchild" - the ' +
- "`unstable_patchRoutesOnMiss()` function threw the following " +
+ "`unstable_patchRoutesOnNavigation()` function threw the following " +
"error:\nError: broke!"
),
true
@@ -1674,7 +2000,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -1713,7 +2039,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
@@ -1761,7 +2087,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "parent",
},
],
- async unstable_patchRoutesOnMiss({ patch }) {
+ async unstable_patchRoutesOnNavigation({ patch }) {
let children = await childrenDfd.promise;
patch("parent", children);
},
@@ -1803,7 +2129,7 @@ describe("Lazy Route Discovery (Fog of War)", () => {
path: "a",
},
],
- async unstable_patchRoutesOnMiss({ matches, patch }) {
+ async unstable_patchRoutesOnNavigation({ matches, patch }) {
await tick();
if (last(matches).route.id === "a") {
patch("a", [
diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts
index d4efd5c92e..cb5e2dc842 100644
--- a/packages/react-router/index.ts
+++ b/packages/react-router/index.ts
@@ -93,7 +93,7 @@ export type {
RouterProps,
RouterProviderProps,
RoutesProps,
- PatchRoutesOnMissFunction as unstable_PatchRoutesOnMissFunction,
+ PatchRoutesOnNavigationFunction as unstable_PatchRoutesOnNavigationFunction,
} from "./lib/components";
export type { NavigateFunction } from "./lib/hooks";
export {
@@ -374,7 +374,7 @@ export { RemixErrorBoundary as UNSAFE_RemixErrorBoundary } from "./lib/dom/ssr/e
/** @internal */
export {
- initFogOfWar as UNSAFE_initFogOfWar,
+ getPatchRoutesOnNavigationFunction as UNSAFE_getPatchRoutesOnNavigationFunction,
useFogOFWarDiscovery as UNSAFE_useFogOFWarDiscovery,
} from "./lib/dom/ssr/fog-of-war";
diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx
index c923dc1dbe..bae812fb35 100644
--- a/packages/react-router/lib/components.tsx
+++ b/packages/react-router/lib/components.tsx
@@ -23,7 +23,7 @@ import type {
} from "./router/router";
import { createRouter } from "./router/router";
import type {
- AgnosticPatchRoutesOnMissFunction,
+ AgnosticPatchRoutesOnNavigationFunction,
DataStrategyFunction,
LazyRouteFunction,
TrackedPromise,
@@ -130,8 +130,8 @@ export function mapRouteProperties(route: RouteObject) {
return updates;
}
-export interface PatchRoutesOnMissFunction
- extends AgnosticPatchRoutesOnMissFunction {}
+export interface PatchRoutesOnNavigationFunction
+ extends AgnosticPatchRoutesOnNavigationFunction {}
/**
* @category Routers
@@ -145,7 +145,7 @@ export function createMemoryRouter(
initialEntries?: InitialEntry[];
initialIndex?: number;
unstable_dataStrategy?: DataStrategyFunction;
- unstable_patchRoutesOnMiss?: PatchRoutesOnMissFunction;
+ unstable_patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;
}
): RemixRouter {
return createRouter({
@@ -159,7 +159,7 @@ export function createMemoryRouter(
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
- unstable_patchRoutesOnMiss: opts?.unstable_patchRoutesOnMiss,
+ unstable_patchRoutesOnNavigation: opts?.unstable_patchRoutesOnNavigation,
}).initialize();
}
diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx
index febaa3ea36..503e4384c3 100644
--- a/packages/react-router/lib/dom-export/hydrated-router.tsx
+++ b/packages/react-router/lib/dom-export/hydrated-router.tsx
@@ -16,7 +16,7 @@ import {
UNSAFE_createRouter as createRouter,
UNSAFE_deserializeErrors as deserializeErrors,
UNSAFE_getSingleFetchDataStrategy as getSingleFetchDataStrategy,
- UNSAFE_initFogOfWar as initFogOfWar,
+ UNSAFE_getPatchRoutesOnNavigationFunction as getPatchRoutesOnNavigationFunction,
UNSAFE_shouldHydrateRouteLoader as shouldHydrateRouteLoader,
UNSAFE_useFogOFWarDiscovery as useFogOFWarDiscovery,
UNSAFE_mapRouteProperties as mapRouteProperties,
@@ -186,13 +186,6 @@ function createHydratedRouter(): RemixRouter {
}
}
- let { enabled: isFogOfWarEnabled, patchRoutesOnMiss } = initFogOfWar(
- ssrInfo.manifest,
- ssrInfo.routeModules,
- ssrInfo.context.isSpaMode,
- ssrInfo.context.basename
- );
-
// We don't use createBrowserRouter here because we need fine-grained control
// over initialization to support synchronous `clientLoader` flows.
let router = createRouter({
@@ -205,9 +198,12 @@ function createHydratedRouter(): RemixRouter {
ssrInfo.manifest,
ssrInfo.routeModules
),
- ...(isFogOfWarEnabled
- ? { unstable_patchRoutesOnMiss: patchRoutesOnMiss }
- : {}),
+ unstable_patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(
+ ssrInfo.manifest,
+ ssrInfo.routeModules,
+ ssrInfo.context.isSpaMode,
+ ssrInfo.context.basename
+ ),
});
ssrInfo.router = router;
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index c37a9b751a..cf236e65fa 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -64,7 +64,7 @@ import {
mergeRefs,
usePrefetchBehavior,
} from "./ssr/components";
-import type { PatchRoutesOnMissFunction } from "../components";
+import type { PatchRoutesOnNavigationFunction } from "../components";
import { Router, mapRouteProperties } from "../components";
import type { RouteObject, NavigateOptions } from "../context";
import {
@@ -123,7 +123,7 @@ interface DOMRouterOpts {
future?: Partial;
hydrationData?: HydrationState;
unstable_dataStrategy?: DataStrategyFunction;
- unstable_patchRoutesOnMiss?: PatchRoutesOnMissFunction;
+ unstable_patchRoutesOnNavigation?: PatchRoutesOnNavigationFunction;
window?: Window;
}
@@ -142,7 +142,7 @@ export function createBrowserRouter(
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
- unstable_patchRoutesOnMiss: opts?.unstable_patchRoutesOnMiss,
+ unstable_patchRoutesOnNavigation: opts?.unstable_patchRoutesOnNavigation,
window: opts?.window,
}).initialize();
}
@@ -162,7 +162,7 @@ export function createHashRouter(
routes,
mapRouteProperties,
unstable_dataStrategy: opts?.unstable_dataStrategy,
- unstable_patchRoutesOnMiss: opts?.unstable_patchRoutesOnMiss,
+ unstable_patchRoutesOnNavigation: opts?.unstable_patchRoutesOnNavigation,
window: opts?.window,
}).initialize();
}
diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts
index 4dfc1ec08f..97977d3cb7 100644
--- a/packages/react-router/lib/dom/ssr/fog-of-war.ts
+++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts
@@ -1,5 +1,5 @@
import * as React from "react";
-import type { PatchRoutesOnMissFunction } from "../../components";
+import type { PatchRoutesOnNavigationFunction } from "../../components";
import type { Router as RemixRouter } from "../../router/router";
import { matchRoutes } from "../../router/utils";
import type { AssetsManifest } from "./entry";
@@ -12,23 +12,18 @@ declare global {
}
}
-type FogOfWarInfo = {
- // Currently rendered links that may need prefetching
- nextPaths: Set;
- // Paths we know the client can already match, so no need to perform client-side
- // matching or prefetching for them. Just an optimization to avoid re-matching
- // on a larger and larger route tree over time
- knownGoodPaths: Set;
- // Routes the server was unable to match - don't ask for them again
- known404Paths: Set;
-};
+// Currently rendered links that may need prefetching
+const nextPaths = new Set();
+
+// FIFO queue of previously discovered routes to prevent re-calling on
+// subsequent navigations to the same path
+const discoveredPathsMaxSize = 1000;
+const discoveredPaths = new Set();
// 7.5k to come in under the ~8k limit for most browsers
// https://stackoverflow.com/a/417184
const URL_LIMIT = 7680;
-let fogOfWar: FogOfWarInfo | null = null;
-
export function isFogOfWarEnabled(isSpaMode: boolean) {
return !isSpaMode;
}
@@ -70,44 +65,28 @@ export function getPartialManifest(
};
}
-export function initFogOfWar(
+export function getPatchRoutesOnNavigationFunction(
manifest: AssetsManifest,
routeModules: RouteModules,
isSpaMode: boolean,
basename: string | undefined
-): {
- enabled: boolean;
- patchRoutesOnMiss?: PatchRoutesOnMissFunction;
-} {
+): PatchRoutesOnNavigationFunction | undefined {
if (!isFogOfWarEnabled(isSpaMode)) {
- return { enabled: false };
+ return undefined;
}
- fogOfWar = {
- nextPaths: new Set(),
- knownGoodPaths: new Set(),
- known404Paths: new Set(),
- };
-
- return {
- enabled: true,
- patchRoutesOnMiss: async ({ path, patch }) => {
- if (
- fogOfWar!.known404Paths.has(path) ||
- fogOfWar!.knownGoodPaths.has(path)
- ) {
- return;
- }
- await fetchAndApplyManifestPatches(
- [path],
- fogOfWar!,
- manifest,
- routeModules,
- isSpaMode,
- basename,
- patch
- );
- },
+ return async ({ path, patch }) => {
+ if (discoveredPaths.has(path)) {
+ return;
+ }
+ await fetchAndApplyManifestPatches(
+ [path],
+ manifest,
+ routeModules,
+ isSpaMode,
+ basename,
+ patch
+ );
};
}
@@ -136,16 +115,21 @@ export function useFogOFWarDiscovery(
return;
}
let url = new URL(path, window.location.origin);
- let { knownGoodPaths, known404Paths, nextPaths } = fogOfWar!;
- if (knownGoodPaths.has(url.pathname) || known404Paths.has(url.pathname)) {
- return;
+ if (!discoveredPaths.has(url.pathname)) {
+ nextPaths.add(url.pathname);
}
- nextPaths.add(url.pathname);
}
// Register and fetch patches for all initially-rendered links/forms
async function fetchPatches() {
- let lazyPaths = getFogOfWarPaths(fogOfWar!);
+ let lazyPaths = Array.from(nextPaths.keys()).filter((path) => {
+ if (discoveredPaths.has(path)) {
+ nextPaths.delete(path);
+ return false;
+ }
+ return true;
+ });
+
if (lazyPaths.length === 0) {
return;
}
@@ -153,7 +137,6 @@ export function useFogOFWarDiscovery(
try {
await fetchAndApplyManifestPatches(
lazyPaths,
- fogOfWar!,
manifest,
routeModules,
isSpaMode,
@@ -214,33 +197,14 @@ export function useFogOFWarDiscovery(
}, [isSpaMode, manifest, routeModules, router]);
}
-function getFogOfWarPaths(fogOfWar: FogOfWarInfo) {
- let { knownGoodPaths, known404Paths, nextPaths } = fogOfWar;
- return Array.from(nextPaths.keys()).filter((path) => {
- if (knownGoodPaths.has(path)) {
- nextPaths.delete(path);
- return false;
- }
-
- if (known404Paths.has(path)) {
- nextPaths.delete(path);
- return false;
- }
-
- return true;
- });
-}
-
export async function fetchAndApplyManifestPatches(
paths: string[],
- _fogOfWar: FogOfWarInfo,
manifest: AssetsManifest,
routeModules: RouteModules,
isSpaMode: boolean,
basename: string | undefined,
patchRoutes: RemixRouter["patchRoutes"]
): Promise {
- let { nextPaths, knownGoodPaths, known404Paths } = _fogOfWar;
let manifestPath = `${basename != null ? basename : "/"}/__manifest`.replace(
/\/+/g,
"/"
@@ -265,16 +229,11 @@ export async function fetchAndApplyManifestPatches(
throw new Error(await res.text());
}
- let data = (await res.json()) as {
- notFoundPaths: string[];
- patches: AssetsManifest["routes"];
- };
-
- // Capture this before we apply the patches to the manifest
- let knownRoutes = new Set(Object.keys(manifest.routes));
+ let serverPatches = (await res.json()) as AssetsManifest["routes"];
// Patch routes we don't know about yet into the manifest
- let patches: AssetsManifest["routes"] = Object.values(data.patches).reduce(
+ let knownRoutes = new Set(Object.keys(manifest.routes));
+ let patches: AssetsManifest["routes"] = Object.values(serverPatches).reduce(
(acc, route) =>
!knownRoutes.has(route.id)
? Object.assign(acc, { [route.id]: route })
@@ -283,11 +242,8 @@ export async function fetchAndApplyManifestPatches(
);
Object.assign(manifest.routes, patches);
- // Track legit 404s so we don't try to fetch them again
- data.notFoundPaths.forEach((p) => known404Paths.add(p));
-
- // Track matched paths so we don't have to fetch them again
- paths.forEach((p) => knownGoodPaths.add(p));
+ // Track discovered paths so we don't have to fetch them again
+ paths.forEach((p) => addToFifoQueue(p, discoveredPaths));
// Identify all parentIds for which we have new children to add and patch
// in their new children
@@ -305,6 +261,14 @@ export async function fetchAndApplyManifestPatches(
);
}
+function addToFifoQueue(path: string, queue: Set) {
+ if (queue.size >= discoveredPathsMaxSize) {
+ let first = queue.values().next().value;
+ queue.delete(first);
+ }
+ queue.add(path);
+}
+
// Thanks Josh!
// https://www.joshwcomeau.com/snippets/javascript/debounce/
function debounce(callback: (...args: unknown[]) => unknown, wait: number) {
diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx
index b27babd7f8..f6adc4ab69 100644
--- a/packages/react-router/lib/hooks.tsx
+++ b/packages/react-router/lib/hooks.tsx
@@ -785,7 +785,7 @@ export function _renderMatches(
dataRouterState.matches.length > 0
) {
// Don't bail if we're initializing with partial hydration and we have
- // router matches. That means we're actively running `patchRoutesOnMiss`
+ // router matches. That means we're actively running `patchRoutesOnNavigation`
// so we should render down the partial matches to the appropriate
// `HydrateFallback`. We only do this if `parentMatches` is empty so it
// only impacts the root matches for `RouterProvider` and no descendant
diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts
index 574005f345..040583c633 100644
--- a/packages/react-router/lib/router/router.ts
+++ b/packages/react-router/lib/router/router.ts
@@ -30,7 +30,7 @@ import type {
Submission,
SuccessResult,
UIMatch,
- AgnosticPatchRoutesOnMissFunction,
+ AgnosticPatchRoutesOnNavigationFunction,
DataWithResponseInit,
} from "./utils";
import {
@@ -369,7 +369,7 @@ export interface RouterInit {
future?: Partial;
hydrationData?: HydrationState;
window?: Window;
- unstable_patchRoutesOnMiss?: AgnosticPatchRoutesOnMissFunction;
+ unstable_patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction;
unstable_dataStrategy?: DataStrategyFunction;
}
@@ -809,7 +809,7 @@ export function createRouter(init: RouterInit): Router {
let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
let basename = init.basename || "/";
let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
- let patchRoutesOnMissImpl = init.unstable_patchRoutesOnMiss;
+ let patchRoutesOnNavigationImpl = init.unstable_patchRoutesOnNavigation;
// Config driven behavior flags
let future: FutureConfig = {
@@ -819,6 +819,10 @@ export function createRouter(init: RouterInit): Router {
let unlistenHistory: (() => void) | null = null;
// Externally-provided functions to call on all state changes
let subscribers = new Set();
+ // FIFO queue of previously discovered routes to prevent re-calling on
+ // subsequent navigations to the same path
+ let discoveredRoutesMaxSize = 1000;
+ let discoveredRoutes = new Set();
// Externally-provided object to hold scroll restoration locations during routing
let savedScrollPositions: Record | null = null;
// Externally-provided function to get scroll restoration keys
@@ -836,7 +840,7 @@ export function createRouter(init: RouterInit): Router {
let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
let initialErrors: RouteData | null = null;
- if (initialMatches == null && !patchRoutesOnMissImpl) {
+ if (initialMatches == null && !patchRoutesOnNavigationImpl) {
// If we do not match a user-provided-route, fall back to the root
// to allow the error boundary to take over
let error = getInternalRouterError(404, {
@@ -847,7 +851,7 @@ export function createRouter(init: RouterInit): Router {
initialErrors = { [route.id]: error };
}
- // In SPA apps, if the user provided a patchRoutesOnMiss implementation and
+ // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and
// our initial match is a splat route, clear them out so we run through lazy
// discovery on hydration in case there's a more accurate lazy route match.
// In SSR apps (with `hydrationData`), we expect that the server will send
@@ -870,7 +874,7 @@ export function createRouter(init: RouterInit): Router {
initialMatches = [];
// If partial hydration and fog of war is enabled, we will be running
- // `patchRoutesOnMiss` during hydration so include any partial matches as
+ // `patchRoutesOnNavigation` during hydration so include any partial matches as
// the initial matches so we can properly render `HydrateFallback`'s
let fogOfWar = checkFogOfWar(
null,
@@ -1009,11 +1013,11 @@ export function createRouter(init: RouterInit): Router {
// we don't need to update UI state if they change
let blockerFunctions = new Map();
- // Map of pending patchRoutesOnMiss() promises (keyed by path/matches) so
+ // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so
// that we only kick them off once for a given combo
let pendingPatchRoutes = new Map<
string,
- ReturnType
+ ReturnType
>();
// Flag to ignore the next history update, so we can revert the URL change on
@@ -3078,7 +3082,14 @@ export function createRouter(init: RouterInit): Router {
routesToUse: AgnosticDataRouteObject[],
pathname: string
): { active: boolean; matches: AgnosticDataRouteMatch[] | null } {
- if (patchRoutesOnMissImpl) {
+ if (patchRoutesOnNavigationImpl) {
+ // Don't bother re-calling patchRouteOnMiss for a path we've already
+ // processed. the last execution would have patched the route tree
+ // accordingly so `matches` here are already accurate.
+ if (discoveredRoutes.has(pathname)) {
+ return { active: false, matches };
+ }
+
if (!matches) {
let fogMatches = matchRoutesImpl(
routesToUse,
@@ -3089,14 +3100,10 @@ export function createRouter(init: RouterInit): Router {
return { active: true, matches: fogMatches || [] };
} else {
- let leafRoute = matches[matches.length - 1].route;
- if (
- leafRoute.path &&
- (leafRoute.path === "*" || leafRoute.path.endsWith("/*"))
- ) {
- // If we matched a splat, it might only be because we haven't yet fetched
- // the children that would match with a higher score, so let's fetch
- // around and find out
+ if (Object.keys(matches[0].params).length > 0) {
+ // If we matched a dynamic param or a splat, it might only be because
+ // we haven't yet discovered other routes that would match with a
+ // higher score. Call patchRoutesOnNavigation just to be sure
let partialMatches = matchRoutesImpl(
routesToUse,
pathname,
@@ -3132,16 +3139,12 @@ export function createRouter(init: RouterInit): Router {
signal: AbortSignal
): Promise {
let partialMatches: AgnosticDataRouteMatch[] | null = matches;
- let route =
- partialMatches.length > 0
- ? partialMatches[partialMatches.length - 1].route
- : null;
while (true) {
let isNonHMR = inFlightDataRoutes == null;
let routesToUse = inFlightDataRoutes || dataRoutes;
try {
await loadLazyRouteChildren(
- patchRoutesOnMissImpl!,
+ patchRoutesOnNavigationImpl!,
pathname,
partialMatches,
routesToUse,
@@ -3169,26 +3172,9 @@ export function createRouter(init: RouterInit): Router {
}
let newMatches = matchRoutes(routesToUse, pathname, basename);
- let matchedSplat = false;
if (newMatches) {
- let leafRoute = newMatches[newMatches.length - 1].route;
-
- if (leafRoute.index) {
- // If we found an index route, we can stop
- return { type: "success", matches: newMatches };
- }
-
- if (leafRoute.path && leafRoute.path.length > 0) {
- if (leafRoute.path === "*") {
- // If we found a splat route, we can't be sure there's not a
- // higher-scoring route down some partial matches trail so we need
- // to check that out
- matchedSplat = true;
- } else {
- // If we found a non-splat route, we can stop
- return { type: "success", matches: newMatches };
- }
- }
+ addToFifoQueue(pathname, discoveredRoutes);
+ return { type: "success", matches: newMatches };
}
let newPartialMatches = matchRoutesImpl(
@@ -3198,26 +3184,30 @@ export function createRouter(init: RouterInit): Router {
true
);
- // If we are no longer partially matching anything, this was either a
- // legit splat match above, or it's a 404. Also avoid loops if the
- // second pass results in the same partial matches
+ // Avoid loops if the second pass results in the same partial matches
if (
!newPartialMatches ||
- partialMatches.map((m) => m.route.id).join("-") ===
- newPartialMatches.map((m) => m.route.id).join("-")
+ (partialMatches.length === newPartialMatches.length &&
+ partialMatches.every(
+ (m, i) => m.route.id === newPartialMatches![i].route.id
+ ))
) {
- return { type: "success", matches: matchedSplat ? newMatches : null };
+ addToFifoQueue(pathname, discoveredRoutes);
+ return { type: "success", matches: null };
}
partialMatches = newPartialMatches;
- route = partialMatches[partialMatches.length - 1].route;
- if (route.path === "*") {
- // The splat is still our most accurate partial, so run with it
- return { type: "success", matches: partialMatches };
- }
}
}
+ function addToFifoQueue(path: string, queue: Set) {
+ if (queue.size >= discoveredRoutesMaxSize) {
+ let first = queue.values().next().value;
+ queue.delete(first);
+ }
+ queue.add(path);
+ }
+
function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
manifest = {};
inFlightDataRoutes = convertRoutesToDataRoutes(
@@ -4384,24 +4374,24 @@ function shouldRevalidateLoader(
}
/**
- * Idempotent utility to execute patchRoutesOnMiss() to lazily load route
+ * Idempotent utility to execute patchRoutesOnNavigation() to lazily load route
* definitions and update the routes/routeManifest
*/
async function loadLazyRouteChildren(
- patchRoutesOnMissImpl: AgnosticPatchRoutesOnMissFunction,
+ patchRoutesOnNavigationImpl: AgnosticPatchRoutesOnNavigationFunction,
path: string,
matches: AgnosticDataRouteMatch[],
routes: AgnosticDataRouteObject[],
manifest: RouteManifest,
mapRouteProperties: MapRoutePropertiesFunction,
- pendingRouteChildren: Map>,
+ pendingRouteChildren: Map>,
signal: AbortSignal
) {
let key = [path, ...matches.map((m) => m.route.id)].join("-");
try {
let pending = pendingRouteChildren.get(key);
if (!pending) {
- pending = patchRoutesOnMissImpl({
+ pending = patchRoutesOnNavigationImpl({
path,
matches,
patch: (routeId, children) => {
@@ -5197,7 +5187,7 @@ function getInternalRouterError(
statusText = "Bad Request";
if (type === "route-discovery") {
errorMessage =
- `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnMiss()\` ` +
+ `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnNavigation()\` ` +
`function threw the following error:\n${message}`;
} else if (method && pathname && routeId) {
errorMessage =
diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts
index fe366043d4..f08019ef87 100644
--- a/packages/react-router/lib/router/utils.ts
+++ b/packages/react-router/lib/router/utils.ts
@@ -222,7 +222,7 @@ export interface DataStrategyFunction {
(args: DataStrategyFunctionArgs): Promise;
}
-export interface AgnosticPatchRoutesOnMissFunction<
+export interface AgnosticPatchRoutesOnNavigationFunction<
M extends AgnosticRouteMatch = AgnosticRouteMatch
> {
(opts: {
diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts
index 3213e83fc5..96bddbe5b4 100644
--- a/packages/react-router/lib/server-runtime/server.ts
+++ b/packages/react-router/lib/server-runtime/server.ts
@@ -163,7 +163,11 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
if (isRedirectResponse(response)) {
let result: SingleFetchResult | SingleFetchResults =
- getSingleFetchRedirect(response.status, response.headers);
+ getSingleFetchRedirect(
+ response.status,
+ response.headers,
+ _build.basename
+ );
if (request.method === "GET") {
result = {
@@ -235,10 +239,7 @@ async function handleManifestRequest(
routes: ServerRoute[],
url: URL
) {
- let data: {
- patches: Record;
- notFoundPaths: string[];
- } = { patches: {}, notFoundPaths: [] };
+ let patches: Record = {};
if (url.searchParams.has("p")) {
for (let path of url.searchParams.getAll("p")) {
@@ -246,14 +247,12 @@ async function handleManifestRequest(
if (matches) {
for (let match of matches) {
let routeId = match.route.id;
- data.patches[routeId] = build.assets.routes[routeId];
+ patches[routeId] = build.assets.routes[routeId];
}
- } else {
- data.notFoundPaths.push(path);
}
}
- return json(data, {
+ return json(patches, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
},
diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts
index 0a7f085c30..e887563b5b 100644
--- a/packages/react-router/lib/server-runtime/single-fetch.ts
+++ b/packages/react-router/lib/server-runtime/single-fetch.ts
@@ -9,13 +9,14 @@ import {
isRouteErrorResponse,
ErrorResponseImpl,
data as routerData,
+ stripBasename,
} from "../router/utils";
-import {
- type SingleFetchRedirectResult,
- type SingleFetchResult,
- type SingleFetchResults,
- SingleFetchRedirectSymbol,
+import type {
+ SingleFetchRedirectResult,
+ SingleFetchResult,
+ SingleFetchResults,
} from "../dom/ssr/single-fetch";
+import { SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch";
import type { AppLoadContext } from "./data";
import { sanitizeError, sanitizeErrors } from "./errors";
import { ServerMode } from "./mode";
@@ -97,7 +98,11 @@ export async function singleFetchAction(
// let non-Response return values through
if (isResponse(result)) {
return {
- result: getSingleFetchRedirect(result.status, result.headers),
+ result: getSingleFetchRedirect(
+ result.status,
+ result.headers,
+ build.basename
+ ),
headers: result.headers,
status: SINGLE_FETCH_REDIRECT_STATUS,
};
@@ -108,7 +113,11 @@ export async function singleFetchAction(
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) {
return {
- result: getSingleFetchRedirect(context.statusCode, headers),
+ result: getSingleFetchRedirect(
+ context.statusCode,
+ headers,
+ build.basename
+ ),
headers,
status: SINGLE_FETCH_REDIRECT_STATUS,
};
@@ -178,7 +187,8 @@ export async function singleFetchLoaders(
result: {
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
result.status,
- result.headers
+ result.headers,
+ build.basename
),
},
headers: result.headers,
@@ -194,7 +204,8 @@ export async function singleFetchLoaders(
result: {
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
context.statusCode,
- headers
+ headers,
+ build.basename
),
},
headers,
@@ -250,10 +261,17 @@ export async function singleFetchLoaders(
export function getSingleFetchRedirect(
status: number,
- headers: Headers
+ headers: Headers,
+ basename: string | undefined
): SingleFetchRedirectResult {
+ let redirect = headers.get("Location")!;
+
+ if (basename) {
+ redirect = stripBasename(redirect, basename) || redirect;
+ }
+
return {
- redirect: headers.get("Location")!,
+ redirect,
status,
revalidate:
// Technically X-Remix-Revalidate isn't needed here - that was an implementation
diff --git a/packages/react-router/package.json b/packages/react-router/package.json
index aa6889a069..9ff9c2ccde 100644
--- a/packages/react-router/package.json
+++ b/packages/react-router/package.json
@@ -42,7 +42,7 @@
"react-router": "workspace:*",
"set-cookie-parser": "^2.6.0",
"source-map": "^0.7.3",
- "turbo-stream": "^2.2.0"
+ "turbo-stream": "2.3.0"
},
"devDependencies": {
"@types/set-cookie-parser": "^2.4.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1561711928..ff82ae8b36 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -548,8 +548,8 @@ importers:
specifier: ^0.7.3
version: 0.7.4
turbo-stream:
- specifier: ^2.2.0
- version: 2.2.0
+ specifier: 2.3.0
+ version: 2.3.0
devDependencies:
'@types/set-cookie-parser':
specifier: ^2.4.1
@@ -13591,8 +13591,8 @@ packages:
yargs: 17.7.2
dev: false
- /turbo-stream@2.2.0:
- resolution: {integrity: sha512-FKFg7A0To1VU4CH9YmSMON5QphK0BXjSoiC7D9yMh+mEEbXLUP9qJ4hEt1qcjKtzncs1OpcnjZO8NgrlVbZH+g==}
+ /turbo-stream@2.3.0:
+ resolution: {integrity: sha512-PhEr9mdexoVv+rJkQ3c8TjrN3DUghX37GNJkSMksoPR4KrXIPnM2MnqRt07sViIqX9IdlhrgtTSyjoVOASq6cg==}
dev: false
/type-check@0.4.0: