由于我计划将来从事其他 Remix 项目,我想写几篇博文作为自己的指南。这是一项正在进行的工作,我将在学习新事物时对其进行更新。
文章目录
- 创建自定义 Remix Hook 访问来自任何组件的加载器数据
- 从任何地方访问 Root 加载程序数据
- 查找所需路由的 ID
- 非根路由加载程序数据
- 根路由加载程序数据
- 定义 404
- 定义根错误边界
- 处理其他错误
- 不要抛出输入错误
- 对用户体验的一条建议
- 创建可重用的表单组件
创建自定义 Remix Hook 访问来自任何组件的加载器数据
访问加载器数据的典型方法是 useLoaderData
钩子,它将获取最近的加载器的数据。这是通过 React Context 实现的,因此您也可以在组件中使用它。
这里有一把脚枪:如果您尝试在不同的路由中重用该组件,它将获取该路由的加载器数据。
为了解决这个问题,Remix 提供了一个 useRouteLoaderData
钩子,让你通过传递其 ID 来获取任何活动路由的加载器数据。请务必注意,这不会导致任何新的加载程序运行,它只是获取数据(如果可用)。
从任何地方访问 Root 加载程序数据
根路由始终处于活动状态,因此它是存储您希望在任何地方可用的数据的好地方。
您可以使用它从任何路由访问根加载程序数据,但如果需要类型安全,则需要导入根加载程序的类型并将其作为类型参数传递给 useRouteLoaderData
。
// some-other-route.tsx
import type { loader as rootLoader } from "./root.tsx"
export default function SomeOtherRoute() {
const rootLoaderData =
useRouteLoaderData<typeof rootLoader>("root")
// …
}
我不是这种方法的忠实粉丝,因为我每次使用它时都必须导入加载器(通常带有别名)并记下路由 ID。虽然根的 ID 始终 “root” 是 ,但其他路由具有不同的 ID。在实践中,t 很容易在 ID 和加载器类型之间出现不匹配。
与其这样做,我更喜欢在我的 root.tsx 文件中创建一个自定义钩子,其中加载器已经可用,并将其导出以供其他文件使用。
// root.tsx
export async function loader() { … }
export function useRootLoaderData() {
return useRouteLoaderData<typeof loader>("root")
}
查找所需路由的 ID
根据您使用的路由约定,特定路由的 ID 可以是任何内容。与其试图记住模式或挖掘清单,我喜欢使用 useMatches()
钩子。
由于 useMatch 返回所有活动路由的数组,因此它还形成了一个完整的列表,其中包含 useRouteLoaderData 将在给定页面上使用的路由。
// some-other-route.tsx
import { useRootLoaderData } from "./root.tsx"
export default function SomeOtherRoute() {
// temporary just to discover the route IDs
const matches = useMatches()
console.log(matches)
// [
// {
// id: 'root',
// pathname: '/',
// data: [Object],
// },
// {
// id: '_layout',
// pathname: '/',
// data: [Object],
// },
// {
// id: '_layout.content',
// pathname: '/content',
// data: null,
// },
// {
// id: '_layout.content.$slug',
// pathname: '/content/remix-route-loader-data',
// data: [Object],
// }
// ]
}
使用中的第一个路由始终是根路由,最后一个路由始终是当前路由。当你这样做时,你通常会寻找中间的那些。在本例中,您可以看到布局路由的 ID 为 _layout ,因此我将使用这种 ID。
知道 ID 后,可以删除此代码。
如果路由没有加载程序,则数据将为 null,但如果尝试访问未处于活动状态的路由,则数据将未定义。这几乎总是开发人员的错误(在错误的位置使用组件),因此,如果未定义,最好抛出错误。
非根路由加载程序数据
// routes/_layout.tsx
export async function loader() { … }
export function useLayoutLoaderData() {
const data = useRouteLoaderData<typeof loader>("_layout")
if (data === undefined) {
throw new Error(
"useLayoutLoaderData must be used within the _layout route or its children",
)
}
return data
}
根路由加载程序数据
// root.tsx
export async function loader() { … }
export function useRootLoaderData() {
return useRouteLoaderData<typeof loader>("root")
}
定义 404
首先,让我们介绍所有网页应用程序中最常见的错误之一,404 Not Found。
您需要做几件事:
- 在路由文件夹的根目录下创建一个新文件 $.tsx 对于找不到其路由的 URL,这实际上是一个包罗万象的。
- 在加载器中使用 404 进行响应 Remix 还不知道您正在使用此包罗万象的路由来处理 404 错误,因此它不会像搜索引擎所期望的那样使用 http 状态代码 404 进行响应。因此,添加一个加载程序并使用 404 进行响应。
export function loader() {
return new Response("Not Found", {
status: 404,
});
}
- 定义您的页面:我正在使用一个简单的组件来显示错误,但如果您愿意,您可以使您的页面更好。
export default function NotFoundPage() {
return <BigStatusMessage
type="error"
message="Not Found"
title="404"
/>;
}
定义根错误边界
这是最后的手段错误边界,当不存在其他错误边界时,将针对错误触发它。您应该拥有的不仅仅是根错误边界。
// root.tsx
export function ErrorBoundary() {
const error = useRouteError();
return (
<html>
<head>
<title>Oh no!</title>
<Meta />
<Links />
</head>
<body>
{/* add the UI you want your users to see */}
<Scripts />
</body>
</html>
);
}
使用 isRouteErrorResponse 检查它是否为 HTTP 错误响应。例如,您可以使用它来检查 error.status。
isRouteErrorResponse 将为加载程序和操作中抛出的响应返回 true。请注意,在我们的 404 页面加载器中,我们只是简单地返回响应而不是抛出它?
处理其他错误
关于如何以及在何处放置其他错误边界的文章已经写了很多,所以我会保持这一点。但我的经验法则是:
您不需要为每个方案使用错误边界
无论在哪里定义 ,都应该定义边界
请记住,只有当出现加载程序错误时,才会在服务器上呈现 ErrorBound。如果存在操作错误,则仅在客户端上呈现该错误。为什么这很重要?如果要天真地捕获和跟踪边界中的错误,则需要实现一些逻辑来确定错误类型。
不要抛出输入错误
首先,您不希望使用 ErrorBoundary 处理输入错误。理想情况下,您希望在用户输入旁边处理它们。
其次,抛出错误将触发handleError,如果您设置了跟踪,则将向错误跟踪服务报告所有输入错误。我向你保证,你不需要知道什么时候有人忘记输入有效的电子邮件,如果你真的需要它,它可能应该是分析的一部分。
对用户体验的一条建议
每当您显示错误时,请为用户提供有关问题所在以及如何解决错误的指导。
例如,不要只显示“出了点问题”。而是显示类似以下内容:“找不到页面<返回主页>”
如果你已经做到了这一点,希望你的错误处理现在处于更好的状态。
创建可重用的表单组件
假设我们要为我们的应用程序构建一个自定义 Form 组件,这个 Form 将在内部执行一些操作,例如使用 CSRF 令牌呈现隐藏的输入,接受重定向以在正文中发送,或者只是应用所有表单都将具有的一些样式。
构建这样的东西的第一种方法可能是包装 Remix 的 Form 组件。
import type { FormProps as RemixFormProps } from "@remix-run/react";
import { Form as RemixForm } from "@remix-run/react";
type FormProps = RemixFormProps & {
redirectTo?: string;
};
export function Form({ redirectTo, children, ...props }: FormProps) {
return (
<RemixForm {...props} className="some classes">
{redirectTo && (
<input type="hidden" name="redirectTo" value={redirectTo} />
)}
<CSRFTokenInput />
{children}
</RemixForm>
);
}
这将起作用,直到您想用 fetcher.Form .为了解决这个问题,我们可以接受组件作为 prop!
type FormProps = RemixFormProps & {
redirectTo?: string;
form?: React.ComponentType<FormProps>;
};
export function Form({
redirectTo,
children,
// here we default to RemixForm
form: Component = RemixForm,
...props
}: FormProps) {
return (
<Component {...props} className="some classes">
{/* content here */}
</Component>
);
}
最后,我们可以在路由或任何其他组件中使用它:
function Route() {
let fetcher = useFetcher();
return (
<>
<Form />; // with the default
<Form form={fetcher.Form} />; // with a fetcher
</>
);
}
此模式与 remix-form 用于让您传递 Form 或提取器的模式相同。表单,来自 Remix 或 React Router。