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 = [];