コンテンツにスキップ

数据加载

このコンテンツはまだ日本語訳がありません。

Remix 的主要功能之一是简化与服务器的交互以将数据导入组件。遵循这些约定,Remix 可以自动:

  • 服务器渲染您的页面
  • 当 JavaScript 加载失败时,能够适应网络条件
  • 在用户与您的网站互动时进行优化,通过仅加载页面变化部分的数据来提高速度
  • 在转换时并行获取数据、JavaScript 模块、CSS 和其他资产,避免导致 UI 不稳定的渲染 + 获取瀑布
  • 通过在 actions 后重新验证,确保 UI 中的数据与服务器上的数据同步
  • 在后退 / 前进点击时实现出色的滚动恢复(甚至跨域)
  • 使用 error-boundary 处理服务器端错误
  • 使用 error-boundary未找到未授权启用可靠的用户体验
  • 帮助您保持 UI 的快乐路径

基础知识

每个路由模块可以导出一个组件和一个 loaderuseLoaderData 将为你的组件提供加载器的数据:

app/routes/products.tsx
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export const loader = async () => {
return json([
{ id: "1", name: "Pants" },
{ id: "2", name: "Jacket" },
]);
};
export default function Products() {
const products = useLoaderData<typeof loader>();
return (
<div>
<h1>Products</h1>
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}

组件在服务器和浏览器中呈现。加载器仅在服务器上运行。这意味着我们的硬编码产品数组不会包含在浏览器包中,并且可以安全地将服务器专用 API 和 SDK 用于数据库、支付处理、内容管理系统等。

如果您的服务器端模块最终位于客户端捆绑包中,请参阅我们的服务器与客户端代码执行 指南。

路由参数

当您使用 $ 命名文件(如 app/routes/users.$userId.tsxapp/routes/users.$userId.projects.$projectId.tsx)时,动态段(以 $ 开头的段)将从 URL 中解析并通过 params 对象传递给您的加载器。

// app/routes/users.$userId.projects.$projectId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
console.log(params.userId);
console.log(params.projectId);
};

给定以下 URL,参数将被解析如下:

URLparams.userIdparams.projectId
/users/123/projects/abc"123""abc"
/users/aec34g/projects/22cba9"aec34g""22cba9"

这些参数对于查找数据最有用:

// app/routes/users.$userId.projects.$projectId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
return json(
await fakeDb.project.findMany({
where: {
userId: params.userId,
projectId: params.projectId,
},
})
);
};

参数类型安全

由于这些参数来自 URL 而不是源代码,因此您无法确定它们是否会被定义。这就是为什么参数的键上的类型是 string | undefined。在使用它们之前进行验证是一种很好的做法,尤其是在 TypeScript 中,这样可以获得类型安全性。使用 invariant 可以轻松实现。

// app/routes/users.$userId.projects.$projectId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import invariant from "tiny-invariant";
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
invariant(params.userId, "Expected params.userId");
invariant(params.projectId, "Expected params.projectId");
params.projectId; // <-- TypeScript now knows this is a string
};

invariant 失败时,你可能不愿意抛出这样的错误,但请记住,在 Remix 中,你知道用户最终会到达 错误边界,在那里他们可以从问题中恢复,而不是损坏的 UI。

外部 API

Remix 在您的服务器上填充了 fetch API,因此您可以非常轻松地从现有 JSON API 中获取数据。您无需亲自管理状态、错误、竞争条件等,只需从加载器(在服务器上)进行获取,然后让 Remix 处理其余部分即可。

app/routes/gists.tsx
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
export async function loader() {
const res = await fetch("https://api.github.com/gists");
return json(await res.json());
}
export default function GistsRoute() {
const gists = useLoaderData<typeof loader>();
return (
<ul>
{gists.map((gist) => (
<li key={gist.id}>
<a href={gist.html_url}>{gist.id}</a>
</li>
))}
</ul>
);
}

当您已经有可用的 API,并且不关心或需要直接连接到 Remix 应用中的数据源时,这非常有用。

数据库

由于 Remix 在您的服务器上运行,因此您可以直接连接到路由模块中的数据库。例如,您可以使用 Prisma 连接到 Postgres 数据库。

app/db.server.ts
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export { db };

然后你的路由可以导入它并对其进行查询:

// app/routes/products.$categoryId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
import { db } from "~/db.server";
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
return json(
await db.product.findMany({
where: {
categoryId: params.categoryId,
},
})
);
};
export default function ProductCategory() {
const products = useLoaderData<typeof loader>();
return (
<div>
<p>{products.length} Products</p>
{/* ... */}
</div>
);
}

如果您使用的是 TypeScript,则可以在调用 useLoaderData 时使用类型推断来使用 Prisma Client 生成的类型。这可以在编写使用加载数据的代码时提供更好的类型安全性和智能感知。

// app/routes/products.$productId.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
import { db } from "~/db.server";
async function getLoaderData(productId: string) {
const product = await db.product.findUnique({
where: {
id: productId,
},
select: {
id: true,
name: true,
imgSrc: true,
},
});
return product;
}
export const loader = async ({
params,
}: LoaderFunctionArgs) => {
return json(await getLoaderData(params.productId));
};
export default function Product() {
const product = useLoaderData<typeof loader>();
return (
<div>
<p>Product {product.id}</p>
{/* ... */}
</div>
);
}

Cloudflare KV

如果您选择 Cloudflare Pages 或 Workers 作为您的环境,Cloudflare Key Value 存储允许您将数据像静态资源一样保存在边缘。

对于 Pages,要开始本地开发,您需要向 package.json 任务添加一个带有命名空间名称的 --kv 参数,因此它看起来像这样:

"dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public --kv PRODUCTS_KV"

对于 Cloudflare Workers 环境,您需要进行一些其他配置

这使得您可以在加载器上下文中使用 PRODUCTS_KV(Cloudflare Pages 适配器会自动将 KV 存储添加到加载器上下文中):

import type { LoaderFunctionArgs } from "@remix-run/cloudflare";
import { json } from "@remix-run/cloudflare";
import { useLoaderData } from "@remix-run/react";
export const loader = async ({
context,
params,
}: LoaderFunctionArgs) => {
return json(
await context.PRODUCTS_KV.get(
`product-${params.productId}`,
{ type: "json" }
)
);
};
export default function Product() {
const product = useLoaderData<typeof loader>();
return (
<div>
<p>Product</p>
{product.name}
</div>
);
}

未找到

在加载数据时,记录未找到的情况很常见。一旦您知道无法按预期呈现组件,就会抛出响应,Remix 将停止在当前加载器中执行代码并切换到最近的错误边界

export const loader = async ({
params,
request,
}: LoaderFunctionArgs) => {
const product = await db.product.findOne({
where: { id: params.productId },
});
if (!product) {
// we know we can't render the component
// so throw immediately to stop executing code
// and show the not found page
throw new Response("Not Found", { status: 404 });
}
const cart = await getCart(request);
return json({
product,
inCart: cart.includes(product.id),
});
};

URL 搜索参数

URL 搜索参数是 URL 中? 后面的部分。其他名称包括查询字符串搜索字符串位置搜索。您可以通过从 request.url 创建 URL 来访问这些值:

app/routes/products.tsx
import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export const loader = async ({
request,
}: LoaderFunctionArgs) => {
const url = new URL(request.url);
const term = url.searchParams.get("term");
return json(await fakeProductSearch(term));
};

这里有几种类型的网络平台在起作用:

  • request 对象具有 url 属性
  • URL 构造函数,用于将 URL 字符串解析为对象
  • url.searchParamsURLSearchParams 的一个实例,它是位置搜索字符串的解析版本,可轻松读取和操作搜索字符串

给定以下 URL,搜索参数将被解析如下:

URLurl.searchParams.get("term")
/products?term=stretchy+pants"stretchy pants"
/products?term=""
/productsnull

数据重新加载

当多个嵌套路由呈现且搜索参数发生变化时,所有路由都将重新加载(而不仅仅是新的或更改的路由)。这是因为搜索参数是一个跨切关注点,可能会影响任何加载器。如果您想在这种情况下阻止某些路由重新加载,请使用 shouldRevalidate

在组件中搜索参数

有时您需要从组件而不是加载器和操作读取和更改搜索参数。根据您的用例,有多种方法可以做到这一点。

设置搜索参数

设置搜索参数最常见的方法可能是让用户通过表单控制它们:

app/routes/products.shoes.tsx
export default function ProductFilters() {
return (
<Form method="get">
<label htmlFor="nike">Nike</label>
<input
type="checkbox"
id="nike"
name="brand"
value="nike"
/>
<label htmlFor="adidas">Adidas</label>
<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
/>
<button type="submit">Update</button>
</Form>
);
}

如果用户只选择了一个:

  • 耐克
  • 阿迪达斯

那么 URL 将为 /products/shoes?brand=nike

如果用户同时选择了:

  • 耐克
  • 阿迪达斯

那么 URL 将是:/products/shoes?brand=nike&brand=adidas

请注意,由于两个复选框均命名为 brand,因此 URL 搜索字符串中会重复出现 brand。在您的加载器中,您可以使用 searchParams.getAll 访问所有这些值

import type { LoaderFunctionArgs } from "@remix-run/node"; // or cloudflare/deno
import { json } from "@remix-run/node"; // or cloudflare/deno
export async function loader({
request,
}: LoaderFunctionArgs) {
const url = new URL(request.url);
const brands = url.searchParams.getAll("brand");
return json(await getProducts({ brands }));
}

链接到搜索参数

作为开发人员,您可以通过链接到包含搜索字符串的 URL 来控制搜索参数。该链接将用链接中的内容替换 URL 中的当前搜索字符串(如果有):

<Link to="?brand=nike">Nike (only)</Link>

读取组件中的搜索参数

除了在加载器中读取搜索参数之外,您还经常需要在组件中访问它们:

import { useSearchParams } from "@remix-run/react";
export default function ProductFilters() {
const [searchParams] = useSearchParams();
const brands = searchParams.getAll("brand");
return (
<Form method="get">
<label htmlFor="nike">Nike</label>
<input
type="checkbox"
id="nike"
name="brand"
value="nike"
defaultChecked={brands.includes("nike")}
/>
<label htmlFor="adidas">Adidas</label>
<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
defaultChecked={brands.includes("adidas")}
/>
<button type="submit">Update</button>
</Form>
);
}

你可能希望在任何字段更改时自动提交表单,为此有useSubmit

import {
useSubmit,
useSearchParams,
} from "@remix-run/react";
export default function ProductFilters() {
const submit = useSubmit();
const [searchParams] = useSearchParams();
const brands = searchParams.getAll("brand");
return (
<Form
method="get"
onChange={(e) => submit(e.currentTarget)}
>
{/* ... */}
</Form>
);
}

强制设置搜索参数

虽然不常见,但您也可以随时出于任何原因强制设置 searchParams。这里的用例很少,以至于我们甚至想不出一个好的例子,但这里有一个愚蠢的例子:

import { useSearchParams } from "@remix-run/react";
export default function ProductFilters() {
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
const id = setInterval(() => {
setSearchParams({ now: Date.now() });
}, 1000);
return () => clearInterval(id);
}, [setSearchParams]);
// ...
}

搜索参数和受控输入

通常,您希望将某些输入(例如复选框)与 URL 中的搜索参数保持同步。对于 React 的受控组件概念,这可能会有点棘手。

仅当搜索参数可以通过两种方式设置,并且我们希望输入与搜索参数保持同步时,才需要这样做。例如,<input type="checkbox">Link 都可以更改此组件中的品牌:

import { useSearchParams } from "@remix-run/react";
export default function ProductFilters() {
const [searchParams] = useSearchParams();
const brands = searchParams.getAll("brand");
return (
<Form method="get">
<p>
<label htmlFor="nike">Nike</label>
<input
type="checkbox"
id="nike"
name="brand"
value="nike"
defaultChecked={brands.includes("nike")}
/>
<Link to="?brand=nike">(only)</Link>
</p>
<button type="submit">Update</button>
</Form>
);
}

如果用户点击复选框并提交表单,URL 会更新,复选框状态也会更改。但如果用户点击链接,则只有 URL 会更新,而复选框不会更新。这不是我们想要的。您可能熟悉这里的 React 受控组件,并考虑将其切换为 checked 而不是 defaultChecked

<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
checked={brands.includes("adidas")}
/>

现在我们遇到了相反的问题:单击链接会同时更新 URL 和复选框状态,但复选框不再起作用,因为 React 会阻止状态更改,直到控制它的 URL 发生更改 —— 而且它永远不会更改,因为我们无法更改复选框并重新提交表单。

React 希望您使用某种状态来控制它,但我们希望用户在提交表单之前控制它,然后我们希望 URL 在它发生变化时控制它。所以我们处于这个有点受控制的位置。

您有两种选择,具体选择取决于您想要的用户体验。

首选:最简单的方法是当用户点击复选框时自动提交表单:

import {
useSubmit,
useSearchParams,
} from "@remix-run/react";
export default function ProductFilters() {
const submit = useSubmit();
const [searchParams] = useSearchParams();
const brands = searchParams.getAll("brand");
return (
<Form method="get">
<p>
<label htmlFor="nike">Nike</label>
<input
type="checkbox"
id="nike"
name="brand"
value="nike"
onChange={(e) => submit(e.currentTarget.form)}
checked={brands.includes("nike")}
/>
<Link to="?brand=nike">(only)</Link>
</p>
{/* ... */}
</Form>
);
}

(如果您还在表单 onChange 上自动提交,请确保 e.stopPropagation()以便事件不会冒泡到表单,否则每次单击复选框时您都会得到双重提交。)

第二种选择:如果您希望输入是半受控的,其中复选框反映 URL 状态,但用户也可以在提交表单和更改 URL 之前将其打开或关闭,则需要连接一些状态。这有点麻烦,但很简单:

  • 从搜索参数中初始化一些状态
  • 当用户点击复选框时更新状态,使复选框变为已选中
  • 当搜索参数发生变化(用户提交表单或点击链接)时更新状态以反映 URL 搜索参数中的内容
import {
useSubmit,
useSearchParams,
} from "@remix-run/react";
export default function ProductFilters() {
const submit = useSubmit();
const [searchParams] = useSearchParams();
const brands = searchParams.getAll("brand");
const [nikeChecked, setNikeChecked] = React.useState(
// initialize from the URL
brands.includes("nike")
);
// Update the state when the params change
// (form submission or link click)
React.useEffect(() => {
setNikeChecked(brands.includes("nike"));
}, [brands, searchParams]);
return (
<Form method="get">
<p>
<label htmlFor="nike">Nike</label>
<input
type="checkbox"
id="nike"
name="brand"
value="nike"
onChange={(e) => {
// update checkbox state w/o submitting the form
setNikeChecked(true);
}}
checked={nikeChecked}
/>
<Link to="?brand=nike">(only)</Link>
</p>
{/* ... */}
</Form>
);
}

您可能希望对复选框进行如下抽象:

<div>
<SearchCheckbox name="brand" value="nike" />
<SearchCheckbox name="brand" value="reebok" />
<SearchCheckbox name="brand" value="adidas" />
</div>;
function SearchCheckbox({ name, value }) {
const [searchParams] = useSearchParams();
const paramsIncludeValue = searchParams
.getAll(name)
.includes(value);
const [checked, setChecked] = React.useState(
paramsIncludeValue
);
React.useEffect(() => {
setChecked(paramsIncludeValue);
}, [paramsIncludeValue]);
return (
<input
type="checkbox"
name={name}
value={value}
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
);
}

选项 3:我们说过只有两个选项,但如果你非常了解 React,那么还有第三个不靠谱的选项可能会吸引你。你可能想删除输入,然后使用 key prop 诡计重新安装它。虽然很聪明,但这会导致可访问性问题,因为当用户单击节点后 React 从文档中删除节点时,用户将失去焦点。

不要这样做,这将导致可访问性问题

<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
key={"adidas" + brands.includes("adidas")}
defaultChecked={brands.includes("adidas")}
/>

Remix 优化

Remix 通过仅加载页面在导航时发生变化的部分数据来优化用户体验。例如,考虑一下您现在在这些文档中使用的 UI。侧面的导航栏位于父路由中,该路由获取了所有文档的动态生成菜单,而子路由获取了您现在正在阅读的文档。如果您单击侧边栏中的链接,Remix 知道父路由将保留在页面上 - 但子路由的数据将发生变化,因为文档的 url 参数将发生变化。有了这种洞察力,Remix 不会重新获取父路由的数据

如果没有 Remix,下一个问题就是如何重新加载所有数据?。这也是 Remix 的内置功能。每当调用 action(用户提交表单或您,程序员,从 useSubmit 调用 submit)时,Remix 都会自动重新加载页面上的所有路由,以捕获可能发生的任何更改。

您不必担心缓存过期,也不必避免在用户与您的应用交互时过度获取数据,因为这一切都是自动的。

在以下三种情况下 Remix 将重新加载所有路由:

  • 执行操作后(表单、useSubmitfetcher.submit
  • 如果 URL 搜索参数发生变化(任何加载器都可以使用它们)
  • 用户点击指向他们当前所在 URL 的链接(这也会替换历史记录堆栈中的当前条目)

所有这些行为都模拟了浏览器的默认行为。在这些情况下,Remix 对您的代码了解不够,无法优化数据加载,但您可以使用 shouldRevalidate 自行优化。

数据库

得益于 Remix 的数据约定和嵌套路由,您通常会发现不需要使用客户端数据库,例如 React Query、SWR、Apollo、Relay、urql 等。如果您使用 redux 等全局状态管理库,主要用于与服务器上的数据交互,那么您也不太可能需要这些库。

当然,Remix 不会阻止您使用它们(除非它们需要捆绑器集成)。您可以引入任何您喜欢的 React 数据库,并在您认为它们比 Remix API 更好地为您的 UI 提供服务的地方使用它们。在某些情况下,您可以使用 Remix 进行初始服务器渲染,然后切换到您最喜欢的库进行后续交互。

也就是说,如果你使用外部数据库并绕过 Remix 自己的数据约定,Remix 就无法再自动

  • 服务器渲染您的页面
  • 当 JavaScript 加载失败时,能够适应网络条件
  • 在用户与您的网站交互时进行优化,通过仅加载页面变化部分的数据来提高速度
  • 在转换时并行获取数据、JavaScript 模块、CSS 和其他资产,避免导致 UI 不稳定的渲染 + 获取瀑布
  • 通过在操作后重新验证,确保 UI 中的数据与服务器上的数据同步
  • 在后退 / 前进点击时实现出色的滚动恢复(甚至跨域)
  • 使用 错误边界 处理服务器端错误
  • 使用 错误边界未找到未授权启用可靠的用户体验
  • 帮助您保持 UI 的快乐路径。

相反,您需要做额外的工作来提供良好的用户体验。

Remix 旨在满足您设计的任何用户体验。虽然您_需要_外部数据库是意料之外的事情,但您可能仍然_想要_一个,这没问题!

当你学习 Remix 时,你会发现你从客户端状态的思考转变为 URL 的思考,当你这样做时,你会免费获得一堆东西。

陷阱

加载器仅在服务器上通过浏览器中的 fetch 调用,因此您的数据将使用 JSON.stringify 序列化并通过网络发送,然后才能到达您的组件。这意味着您的数据需要可序列化。例如:

这不起作用!

export async function loader() {
return {
date: new Date(),
someMethod() {
return "hello!";
},
};
}
export default function RouteComp() {
const data = useLoaderData<typeof loader>();
console.log(data);
// '{"date":"2021-11-27T23:54:26.384Z"}'
}

并非所有东西都能成功!加载器用于_数据_,而数据需要可序列化。

某些数据库(如 FaunaDB)会返回带有方法的对象,您需要在从加载程序返回之前小心地对这些对象进行序列化。通常这不是问题,但最好了解您的数据正在通过网络传输。

此外,Remix 将为您调用加载器,在任何情况下您都不应尝试直接调用加载器:

这不起作用

export const loader = async () => {
return json(await fakeDb.products.findMany());
};
export default function RouteComp() {
const data = loader();
// ...
}