You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Remix React中不中断loader渲染自定义404页面的问题

解决Remix中CatchBoundary无法获取Root Loader数据的问题

你的核心问题是:当子路由抛出404错误时,Remix会中断整个loader执行链,导致Root组件的CatchBoundary无法获取Root Loader返回的布局数据,进而无法渲染依赖该数据的Layout组件。下面提供几种可行的解决思路:

方案1:用嵌套路由拆分布局与业务路由

将布局相关的Loader逻辑移到独立的父路由中,利用Remix嵌套路由的执行机制——父路由Loader会在子路由Loader前执行,即使子路由抛出错误,父路由的Loader数据依然可用。

步骤1:调整路由结构

  • 创建app/routes/__layout.jsx作为布局路由(前缀__表示该路由不生成URL路径)
  • 修改app/root.jsx仅负责渲染最外层HTML结构
  • 原业务路由(包括$.jsx)作为__layout的子路由

代码实现

app/root.jsx

import { Meta, Links, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

app/routes/__layout.jsx

import { Outlet, useLoaderData, useCatch } from "@remix-run/react";
import Layout from "~/components/Layout";
import { getLayoutContent } from "~/utils/getLayoutContent";

// 封装布局数据获取逻辑到独立函数,方便复用
export async function loader({ context }) {
  const layout = await context.storefront.query(LAYOUT_QUERY);
  const layoutContent = await getLayoutContent(context);
  return defer({ layout, layoutContent });
}

export default function LayoutRoute() {
  const { layout, layoutContent } = useLoaderData();
  return (
    <Layout layout={layout} content={layoutContent}>
      <Outlet />
    </Layout>
  );
}

// 布局路由的CatchBoundary,能获取到当前路由Loader返回的数据
export function CatchBoundary() {
  const { layoutContent } = useLoaderData();
  const caught = useCatch();

  if (caught.status === 404) {
    return (
      <Layout content={layoutContent}>
        <p>Custom Error Page</p>
      </Layout>
    );
  }

  // 通用错误页面
  return <div>Oops, something went wrong!</div>;
}

app/utils/getLayoutContent.js

export async function getLayoutContent(context) {
  const [headerNavLinks, footerMenus, footerBottomLinks, newsletterCopy] =
    await Promise.all([
      getHeaderContent(),
      getFooterMenus(),
      getFooterBottomLinks(),
      getNewsLetterCopy(),
    ]);

  return {
    headerContent: { headerNavLinks },
    footerContent: { footerMenus, footerBottomLinks, newsletterCopy },
  };
}

这样,当$.jsx抛出404时,__layout的Loader已经执行完成,CatchBoundary可以正常获取到布局数据,渲染带Layout的404页面。

方案2:在CatchBoundary中独立获取布局数据

如果不想调整路由结构,可以将布局数据的获取逻辑封装成独立API,在CatchBoundary中通过客户端请求获取数据。

步骤1:创建布局数据API路由

// app/routes/api/layout-content.jsx
import { getLayoutContent } from "~/utils/getLayoutContent";

export async function loader({ context }) {
  const layoutContent = await getLayoutContent(context);
  return layoutContent;
}

步骤2:在Root的CatchBoundary中请求数据

// app/root.jsx
import { useState, useEffect } from "react";
import { useCatch } from "@remix-run/react";
import Layout from "~/components/Layout";

export function CatchBoundary() {
  const caught = useCatch();
  const [layoutContent, setLayoutContent] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchLayout() {
      try {
        const res = await fetch("/api/layout-content");
        if (!res.ok) throw new Error("Failed to load layout data");
        const data = await res.json();
        setLayoutContent(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchLayout();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Failed to load page layout</div>;

  if (caught.status === 404) {
    return (
      <Layout content={layoutContent}>
        <p>Custom Error Page</p>
      </Layout>
    );
  }

  return <div>Oops, something went wrong!</div>;
}

这种方案无需调整路由,但会多一次客户端请求,适合无法修改路由结构的场景。

方案3:为Layout组件添加默认数据降级

如果布局数据不是必须的(或可以使用默认值),可以修改Layout组件,当content为undefined时使用默认数据,避免报错:

// app/components/Layout.jsx
import Header from './Header';
import Footer from './Footer';

// 定义默认布局数据
const DEFAULT_CONTENT = {
  headerContent: { headerNavLinks: [] },
  footerContent: { footerMenus: [], footerBottomLinks: [], newsletterCopy: "" }
};

export default function Layout({children, content = DEFAULT_CONTENT}) {
  const {headerContent, footerContent} = content;

  return (
    <div className="flex flex-col min-h-screen">
      <div>
        <a href="#mainContent" className="sr-only">
          Skip to content
        </a>
      </div>
      <Header headerContent={headerContent} />
      <main role="main" id="mainContent" className="flex-grow bg-primary7">
        {children}
      </main>
      <Footer content={footerContent} />
    </div>
  );
}

这种方案最简单,但只能作为临时降级方案,无法展示真实的布局数据。


内容的提问来源于stack exchange,提问作者avocado

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 15:30:53