主分支
分支
main (6.23.1)dev
版本
6.23.1v4/5.xv3.x
errorElement
本页内容

errorElement

当在 加载器操作 或组件渲染中抛出异常时,您的路由的正常渲染路径 (<Route element>) 将不会被执行,而是会渲染错误路径 (<Route errorElement>),并且错误信息将通过 useRouteError 提供。

如果您不想指定 React 元素(例如,errorElement={<MyErrorBoundary />}),您可以改为指定一个 ErrorBoundary 组件(例如,ErrorBoundary={MyErrorBoundary}),React Router 会在内部为您调用 createElement

此功能仅在使用数据路由时有效,请参阅 选择路由器

<Route
  path="/invoices/:id"
  // if an exception is thrown here
  loader={loadInvoice}
  // here
  action={updateInvoice}
  // or here
  element={<Invoice />}
  // this will render instead of `element`
  errorElement={<ErrorBoundary />}
/>;

function Invoice() {
  return <div>Happy {path}</div>;
}

function ErrorBoundary() {
  let error = useRouteError();
  console.error(error);
  // Uncaught ReferenceError: path is not defined
  return <div>Dang!</div>;
}

冒泡

当路由没有 errorElement 时,错误会向上冒泡到父路由。这使您可以根据需要进行细粒度或更通用的处理。

在您的路由树的顶部放置一个 errorElement,并在一个地方处理几乎所有应用程序中的错误。或者,将它们放在所有路由上,并允许没有错误的应用程序部分继续正常渲染。这为用户提供了更多从错误中恢复的选项,而不是硬刷新和 🤞。

默认错误元素

我们建议在将应用程序发布到生产环境之前始终至少提供一个根级别的 errorElement,因为默认 errorElement 的 UI 很丑陋,不适合最终用户使用。

如果您在路由树中没有提供 errorElement 来处理给定的错误,错误会向上冒泡并由默认的 errorElement 处理,该元素会打印错误消息和堆栈跟踪。有些人质疑为什么堆栈跟踪会在生产版本中显示。通常,出于安全原因,您不希望在生产网站上公开堆栈跟踪。但是,这更适用于服务器端错误(Remix 确实会从服务器端加载器/操作响应中删除堆栈跟踪)。在客户端 react-router-dom 应用程序的情况下,代码已经存在于浏览器中,因此任何隐藏都只是安全上的模糊处理。此外,我们仍然希望在控制台中公开错误,因此从 UI 显示中删除它仍然不会隐藏有关堆栈跟踪的任何信息。不在 UI 中显示并且不在控制台中记录它意味着应用程序开发人员完全没有关于生产错误的信息,这会带来自身的一系列问题。因此,我们再次建议您在将网站部署到生产环境之前始终添加一个根级别的 errorElement

手动抛出异常

虽然 errorElement 处理意外错误,但它也可以用于处理您预期的异常。

特别是在加载器和操作中,您处理的是不受您控制的外部数据,您无法始终计划数据存在、服务可用或用户有权访问它。在这些情况下,您可以抛出您自己的异常。

以下是在 加载器 中的“未找到”情况

<Route
  path="/properties/:id"
  element={<PropertyForSale />}
  errorElement={<PropertyError />}
  loader={async ({ params }) => {
    const res = await fetch(`/api/properties/${params.id}`);
    if (res.status === 404) {
      throw new Response("Not Found", { status: 404 });
    }
    const home = await res.json();
    const descriptionHtml = parseMarkdown(
      data.descriptionMarkdown
    );
    return { home, descriptionHtml };
  }}
/>

一旦您知道无法使用您正在加载的数据渲染路由,就可以抛出异常来中断调用堆栈。当数据不存在时,您不必担心加载器中剩余的工作(例如解析用户的 Markdown 简介)。只需抛出异常并退出。

这也意味着您不必担心在路由组件中进行大量错误分支代码。如果您在加载器或操作中抛出异常,它甚至不会尝试渲染,因为您的 errorElement 将会渲染。

您可以从加载器或操作中抛出任何内容,就像您可以返回任何内容一样:响应(如前面的示例)、错误或普通对象。

抛出响应

虽然您可以抛出任何内容,并且它将通过 useRouteError 返回给您,但如果您抛出一个 Response,React Router 会在将数据返回给您的组件之前自动解析响应数据。

此外,isRouteErrorResponse 允许您在边界中检查这种特定类型。结合 json,您可以轻松地抛出包含一些数据的响应,并在边界中渲染不同的情况

import { json } from "react-router-dom";

function loader() {
  const stillWorksHere = await userStillWorksHere();
  if (!stillWorksHere) {
    throw json(
      {
        sorry: "You have been fired.",
        hrEmail: "[email protected]",
      },
      { status: 401 }
    );
  }
}

function ErrorBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error) && error.status === 401) {
    // the response json is automatically parsed to
    // `error.data`, you also have access to the status
    return (
      <div>
        <h1>{error.status}</h1>
        <h2>{error.data.sorry}</h2>
        <p>
          Go ahead and email {error.data.hrEmail} if you
          feel like this is a mistake.
        </p>
      </div>
    );
  }

  // rethrow to let the parent error boundary handle it
  // when it's not a special case for this route
  throw error;
}

这使得创建通用的错误边界成为可能,通常在您的根路由上,它处理许多情况

function RootBoundary() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return <div>This page doesn't exist!</div>;
    }

    if (error.status === 401) {
      return <div>You aren't authorized to see this</div>;
    }

    if (error.status === 503) {
      return <div>Looks like our API is down</div>;
    }

    if (error.status === 418) {
      return <div>🫖</div>;
    }
  }

  return <div>Something went wrong</div>;
}

抽象

这种在您知道无法继续当前数据加载路径时抛出异常的模式使正确处理异常情况变得非常简单。

想象一个函数,它为授权请求获取用户的 Web 令牌,看起来像这样

async function getUserToken() {
  const token = await getTokenFromWebWorker();
  if (!token) {
    throw new Response("", { status: 401 });
  }
  return token;
}

无论哪个加载器或操作使用该函数,它都会停止执行当前调用堆栈中的代码,并将应用程序发送到错误路径。

现在让我们添加一个获取项目的函数

function fetchProject(id) {
  const token = await getUserToken();
  const response = await fetch(`/projects/${id}`, {
    headers: { Authorization: `Bearer ${token}` },
  });

  if (response.status === 404) {
    throw new Response("Not Found", { status: 404 });
  }

  // the fetch failed
  if (!response.ok) {
    throw new Error("Could not fetch project");
  }
}

由于 getUserToken,这段代码可以假设它获取了一个令牌。如果没有,错误路径将被渲染。然后,如果项目不存在,无论哪个加载器调用此函数,它都会抛出一个 404 到 errorElement。最后,如果获取完全失败,它会发送一个错误。

在您意识到“我没有我需要的东西”的任何时候,您都可以简单地抛出,知道您仍然为最终用户渲染了一些有用的东西。

让我们把它组合成一个路由

<Route
  path="/"
  element={<Root />}
  errorElement={<RootBoundary />}
>
  <Route
    path="projects/:projectId"
    loader={({ params }) => fetchProject(params.projectId)}
    element={<Project />}
  />
</Route>

项目路由根本不必考虑错误。在加载器实用程序函数(如 fetchProjectgetUserToken)在出现问题时抛出异常,以及 RootBoundary 处理所有情况之间,项目路由可以专注于正常路径。

文档和示例 CC 4.0