在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
相关产品推荐
相关产品推荐

