Remix 的主要功能之一是简化与服务器的交互,将数据加载到组件中。 当您遵循这些约定,Remix 可以自动执行以下操作:
每个路由模块都可以导出一个组件和一个 loader
。 useLoaderData
会将 loader 的数据提供给您的组件
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>
);
}
该组件在服务器和浏览器上呈现。 loader *仅在服务器上运行*。 这意味着我们硬编码的 products 数组不会包含在浏览器捆绑包中,并且可以安全地使用仅限服务器的 API 和 SDK,用于数据库、支付处理、内容管理系统等。
如果您的服务器端模块最终出现在客户端捆绑包中,请参阅我们的关于 服务器与客户端代码执行 的指南。
当您使用 $
命名文件时,例如 app/routes/users.$userId.tsx
和 app/routes/users.$userId.projects.$projectId.tsx
,动态片段(以 $
开头的片段)将从 URL 中解析出来,并以 params
对象的形式传递给您的加载器。
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,参数将按如下方式解析:
URL | params.userId |
params.projectId |
---|---|---|
/users/123/projects/abc |
"123" |
"abc" |
/users/aec34g/projects/22cba9 |
"aec34g" |
"22cba9" |
这些参数对于查找数据非常有用。
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
可以很容易地做到这一点。
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。
Remix 在您的服务器上填充了 fetch
API,因此很容易从现有的 JSON API 中获取数据。您可以从加载器(在服务器上)执行获取操作,并让 Remix 处理其余的事情,而不是自己管理状态、错误、竞争条件等等。
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 数据库。
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export { db };
然后您的路由可以导入它并对其进行查询。
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 生成的类型。这使得在编写使用加载数据的代码时可以获得更好的类型安全性和智能感知。
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 Pages 或 Workers 作为您的环境,Cloudflare 键值存储允许您像持久化静态资源一样在边缘持久化数据。
对于 Pages,要开始本地开发,您需要在 package.json 任务中添加一个带有命名空间名称的 --kv
参数,如下所示:
"dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public --kv PRODUCTS_KV"
对于 Cloudflare Workers 环境,您需要进行其他一些配置。
这使您可以在加载器上下文中使用 PRODUCTS_KV
(KV 存储由 Cloudflare Pages 适配器自动添加到加载器上下文中)。
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>
);
}
加载数据时,通常会发生记录“未找到”的情况。一旦您知道无法按预期渲染组件,就 throw
一个响应,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 中 ?
之后的部分。其他名称包括“查询字符串”、“搜索字符串”或“位置搜索”。您可以通过从 request.url
创建 URL 来访问这些值。
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));
};
这里有几个 Web 平台类型在起作用:
request
对象有一个 url
属性。url.searchParams
是 URLSearchParams 的一个实例,它是位置搜索字符串的解析版本,可以轻松读取和操作搜索字符串。给定以下 URL,搜索参数将按如下方式解析:
URL | url.searchParams.get("term") |
---|---|
/products?term=stretchy+pants |
"stretchy pants" |
/products?term= |
"" |
/products |
null |
当渲染多个嵌套路由并且搜索参数发生更改时,所有路由都将重新加载(而不仅仅是新的或更改的路由)。这是因为搜索参数是一个跨领域的问题,可能会影响任何加载器。如果您想防止某些路由在这种情况下重新加载,请使用shouldRevalidate。
有时您需要从组件而不是加载器和操作中读取和更改搜索参数。根据您的用例,有多种方法可以做到这一点。
设置搜索参数
设置搜索参数最常见的方法是让用户使用表单控制它们。
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>
);
}
命令式地设置搜索参数
虽然不常见,但您也可以随时出于任何原因命令式地设置搜索参数。这里的用例很少,我们甚至想不出一个好的例子,但这里有一个简单的例子:
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 之前打开和关闭它,您需要连接一些状态。这有点工作,但很简单。
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 通过仅加载页面上导航时正在更改的部分的数据来优化用户体验。例如,考虑一下您现在在这些文档中使用的 UI。侧面的导航栏位于一个父路由中,该路由获取了所有文档的动态生成的菜单,而子路由则获取了您现在正在阅读的文档。如果您单击侧边栏中的链接,Remix 知道父路由将保留在页面上 - 但子路由的数据将更改,因为文档的 url 参数将更改。有了这个洞察,Remix 不会重新获取父路由的数据。
没有 Remix 的下一个问题是“我如何重新加载所有数据?”。这也内置在 Remix 中。每当调用action(用户提交了表单或您,程序员,从 useSubmit
调用了 submit
)时,Remix 将自动重新加载页面上的所有路由,以捕获可能发生的任何更改。
您不必担心过期的缓存或避免在用户与您的应用程序交互时过度获取数据,这都是自动的。
在以下三种情况下,Remix 将重新加载您的所有路由:
useSubmit
,fetcher.submit
)。所有这些行为都模拟了浏览器的默认行为。在这些情况下,Remix 对您的代码了解不足以优化数据加载,但您可以使用 shouldRevalidate 自己对其进行优化。
由于 Remix 的数据约定和嵌套路由,您通常会发现您不需要使用诸如 React Query、SWR、Apollo、Relay、urql
等客户端数据库。如果您主要使用诸如 redux 之类的全局状态管理库来与服务器上的数据进行交互,那么您也可能不需要这些库。
当然,Remix 不会阻止您使用它们(除非它们需要捆绑器集成)。您可以引入任何您喜欢的 React 数据库,并在您认为它们比 Remix API 更能为您的 UI 服务的地方使用它们。在某些情况下,您可以使用 Remix 进行初始服务器渲染,然后在之后切换到您最喜欢的库进行交互。
也就是说,如果您引入外部数据库并绕过 Remix 自己的数据约定,Remix 将无法再自动:
相反,您需要做额外的工作来提供良好的用户体验。
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();
// ...
}