diff --git a/.changeset/link-meta-short-circuit.md b/.changeset/link-meta-short-circuit.md
new file mode 100644
index 00000000000..c7d925616d5
--- /dev/null
+++ b/.changeset/link-meta-short-circuit.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/react": patch
+---
+
+short circuit links and meta for routes that are not rendered due to errors
diff --git a/integration/link-test.ts b/integration/link-test.ts
index 48b69655145..3a9ecc4946c 100644
--- a/integration/link-test.ts
+++ b/integration/link-test.ts
@@ -220,6 +220,9 @@ test.describe("route module link export", () => {
Resource routes
+
+ Errored child route
+
@@ -471,6 +474,42 @@ test.describe("route module link export", () => {
}
`,
+
+ "app/routes/parent.jsx": js`
+ import { Outlet } from "@remix-run/react";
+
+ export function links() {
+ return [
+ { "data-test-id": "red" },
+ ];
+ }
+
+ export default function Component() {
+ return
;
+ }
+
+ export function ErrorBoundary() {
+ return Error Boundary ;
+ }
+ `,
+
+ "app/routes/parent.child.jsx": js`
+ import { Outlet } from "@remix-run/react";
+
+ export function loader() {
+ throw new Response(null, { status: 404 });
+ }
+
+ export function links() {
+ return [
+ { "data-test-id": "blue" },
+ ];
+ }
+
+ export default function Component() {
+ return
;
+ }
+ `,
},
});
appFixture = await createAppFixture(fixture);
@@ -511,6 +550,17 @@ test.describe("route module link export", () => {
expect(stylesheetResponses.length).toEqual(1);
});
+ test("does not render errored child route links", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+ await page.click('a[href="/parent/child"]');
+ await page.waitForSelector('[data-test-id="/parent:error-boundary"]');
+ await page.waitForSelector('[data-test-id="red"]', { state: "attached" });
+ await page.waitForSelector('[data-test-id="blue"]', {
+ state: "detached",
+ });
+ });
+
test.describe("no js", () => {
test.use({ javaScriptEnabled: false });
@@ -534,6 +584,16 @@ test.describe("route module link export", () => {
let locator = page.locator("link[rel=preload][as=image]");
expect(await locator.getAttribute("imagesizes")).toBe("100vw");
});
+
+ test("does not render errored child route links", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/parent/child");
+ await page.waitForSelector('[data-test-id="/parent:error-boundary"]');
+ await page.waitForSelector('[data-test-id="red"]', { state: "attached" });
+ await page.waitForSelector('[data-test-id="blue"]', {
+ state: "detached",
+ });
+ });
});
test.describe("script imports", () => {
diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx
index cbcf9480395..c4077f16109 100644
--- a/packages/remix-react/components.tsx
+++ b/packages/remix-react/components.tsx
@@ -393,7 +393,14 @@ let fetcherSubmissionWarning =
*/
export function Links() {
let { manifest, routeModules } = useRemixContext();
- let { matches } = useDataRouterStateContext();
+ let { errors, matches: routerMatches } = useDataRouterStateContext();
+
+ let matches = errors
+ ? routerMatches.slice(
+ 0,
+ routerMatches.findIndex((m) => errors![m.route.id]) + 1
+ )
+ : routerMatches;
let links = React.useMemo(
() => getLinksForMatches(matches, routeModules, manifest),
@@ -578,9 +585,20 @@ function PrefetchPageLinksImpl({
*/
function V1Meta() {
let { routeModules } = useRemixContext();
- let { matches, loaderData } = useDataRouterStateContext();
+ let {
+ errors,
+ matches: routerMatches,
+ loaderData,
+ } = useDataRouterStateContext();
let location = useLocation();
+ let matches = errors
+ ? routerMatches.slice(
+ 0,
+ routerMatches.findIndex((m) => errors![m.route.id]) + 1
+ )
+ : routerMatches;
+
let meta: V1_HtmlMetaDescriptor = {};
let parentsData: { [routeId: string]: AppData } = {};
@@ -676,9 +694,20 @@ function V1Meta() {
function V2Meta() {
let { routeModules } = useRemixContext();
- let { matches: _matches, loaderData } = useDataRouterStateContext();
+ let {
+ errors,
+ matches: routerMatches,
+ loaderData,
+ } = useDataRouterStateContext();
let location = useLocation();
+ let _matches = errors
+ ? routerMatches.slice(
+ 0,
+ routerMatches.findIndex((m) => errors![m.route.id]) + 1
+ )
+ : routerMatches;
+
let meta: V2_MetaDescriptor[] = [];
let leafMeta: V2_MetaDescriptor[] | null = null;
let matches: V2_MetaMatches = [];