¥Data Loading
Remix 的主要功能之一是简化与服务器的交互,以便将数据导入组件。遵循这些约定,Remix 可以自动:
¥One of the primary features of Remix is simplifying interactions with the server to get data into components. When you follow these conventions, Remix can automatically:
服务器渲染你的页面
¥Server render your pages
当 JavaScript 加载失败时,要能够灵活应对网络状况
¥Be resilient to network conditions when JavaScript fails to load
在用户与你的网站交互时进行优化,通过仅加载页面变化部分的数据来提高网站速度。
¥Make optimizations as the user interacts with your site to make it fast by only loading data for the changing parts of the page
在转换过程中并行获取数据、JavaScript 模块、CSS 和其他资源,避免渲染+获取瀑布流导致 UI 卡顿。
¥Fetch data, JavaScript modules, CSS, and other assets in parallel on transitions, avoiding render+fetch waterfalls that lead to choppy UI
通过在 actions 之后重新验证,确保 UI 中的数据与服务器上的数据同步
¥Ensure the data in the UI is in sync with the data on the server by revalidating after actions
出色的后退/前进滚动恢复(即使跨域)
¥Excellent scroll restoration on back/forward clicks (even across domains)
使用 错误边界 处理服务器端错误
¥Handle server-side errors with error boundaries
使用 错误边界 为 "未找到" 和 "未经授权" 启用可靠的用户体验
¥Enable solid UX for "Not Found" and "Unauthorized" with error boundaries
帮助你保持 UI 的良好运行
¥Help you keep the happy path of your UI happy
¥Basics
每个路由模块都可以导出一个组件和一个 loader
。useLoaderData
会将加载器的数据提供给你的组件:
¥Each route module can export a component and a loader
. useLoaderData
will provide the loader's data to your component:
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。
¥The component renders on the server and in the browser. The loader only runs on the server. That means our hard-coded products array doesn't get included in the browser bundles, and it's safe to use server-only for APIs and SDKs for things like database, payment processing, content management systems, etc.
如果你的服务器端模块最终包含在客户端包中,请参阅我们关于 服务器与客户端代码执行 的指南。
¥If your server-side modules end up in client bundles, refer to our guide on server vs. client code execution.
¥Route Params
当你使用 $
命名文件(例如 app/routes/users.$userId.tsx
和 app/routes/users.$userId.projects.$projectId.tsx
)时,动态段(以 $
开头的段)将从 URL 解析,并通过 params
对象传递给加载器。
¥When you name a file with $
like app/routes/users.$userId.tsx
and app/routes/users.$userId.projects.$projectId.tsx
the dynamic segments (the ones starting with $
) will be parsed from the URL and passed to your loader on a params
object.
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,参数将按如下方式解析:
¥Given the following URLs, the params would be parsed as follows:
URL | params.userId |
params.projectId |
---|---|---|
/users/123/projects/abc |
"123" |
"abc" |
/users/aec34g/projects/22cba9 |
"aec34g" |
"22cba9" |
这些参数对于查找数据最有用:
¥These params are most useful for looking up data:
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,
},
})
);
};
¥Param Type Safety
由于这些参数来自 URL 而不是你的源代码,因此你无法确定它们是否会被定义。这就是为什么参数键的类型是 string | undefined
。在使用之前进行验证是一种很好的做法,尤其是在 TypeScript 中,这样可以确保类型安全。使用 invariant
使其变得简单。
¥Because these params come from the URL and not your source code, you can't know for sure if they will be defined. That's why the types on the param's keys are string | undefined
. It's good practice to validate before using them, especially in TypeScript to get type safety. Using invariant
makes it easy.
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。
¥While you may be uncomfortable throwing errors like this with invariant
when it fails, remember that in Remix you know the user will end up in the error boundary where they can recover from the problem instead of a broken UI.
¥External APIs
Remix 在你的服务器上填充了 fetch
API,因此可以直接从现有的 JSON API 中获取数据。你无需亲自管理状态、错误、竞争条件等,只需从服务器上的加载器获取数据,其余部分交给 Remix 处理即可。
¥Remix polyfills the fetch
API on your server, so it's straightforward to fetch data from existing JSON APIs. Instead of managing state, errors, race conditions, and more yourself, you can do the fetch from your loader (on the server) and let Remix handle the rest.
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 应用中直接连接到数据源时,这非常有用。
¥This is great when you already have an API to work with and don't care or need to connect directly to your data source in your Remix app.
¥Databases
由于 Remix 在你的服务器上运行,因此你可以在路由模块中直接连接到数据库。例如,你可以使用 Prisma 连接到 Postgres 数据库。
¥Since Remix runs on your server, you can connect directly to a database in your route modules. For example, you could connect to a Postgres database with Prisma.
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
export { db };
然后你的路由就可以导入它并对其进行查询:
¥And then your routes can import it and make queries against it:
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 客户端生成的类型。这在编写使用加载数据的代码时可以提供更好的类型安全性和智能感知。
¥If you are using TypeScript, you can use type inference to use Prisma Client generated types when calling useLoaderData
. This allows better type safety and intellisense when writing code that uses the loaded data.
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 键值对 存储允许你将数据像静态资源一样持久保存在边缘。
¥If you picked Cloudflare Pages or Workers as your environment, Cloudflare Key Value storage allows you to persist data at the edge as if it were a static resource.
对于 Pages,要开始本地开发,你需要在 package.json 任务中添加一个包含命名空间名称的 --kv
参数,如下所示:
¥For Pages, to start with local development, you need to add a --kv
parameter with a name of your namespace to the package.json task, so it would look like this:
"dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public --kv PRODUCTS_KV"
对于 Cloudflare Workers 环境,你需要 进行其他配置。
¥For the Cloudflare Workers environment you'll need to do some other configuration.
这使你可以在加载器上下文中使用 PRODUCTS_KV
(Cloudflare Pages 适配器会自动将键值存储添加到加载器上下文中):
¥This enables you to use the PRODUCTS_KV
in a loader context (KV stores are added to loader context automatically by the Cloudflare Pages adapter):
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>
);
}
¥Not Found
加载数据时,记录通常为 "未找到"。一旦你发现无法按预期渲染组件,就会抛出一个 throw
响应,Remix 将停止在当前加载器中执行代码,并切换到最近的 错误边界。
¥While loading data, it's common for a record to be "not found". As soon as you know you can't render the component as expected, throw
a response and Remix will stop executing code in the current loader and switch over to the nearest error boundary.
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 Search Params
URL 搜索参数是 URL 中 ?
之后的部分。其他名称包括 "查询字符串"、"搜索字符串" 或 "位置搜索"。你可以通过从 request.url
创建 URL 来访问值:
¥URL Search Params are the portion of the URL after a ?
. Other names for this are "query string", "search string", or "location search". You can access the values by creating a URL out of the request.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 平台:
¥There are a few web platform types at play here:
request
对象具有 url
属性
¥The request
object has a url
property
URL 构造函数 将 URL 字符串解析为对象
¥URL constructor that parses the URL string into an object
url.searchParams
是 URLSearchParams 的一个实例,它是位置搜索字符串的解析版本,使其易于读取和操作。
¥url.searchParams
is an instance of URLSearchParams, which is a parsed version of the location search string that makes it easy to read and manipulate the search string
给定以下 URL,搜索参数将按如下方式解析:
¥Given the following URLs, the search params would be parsed as follows:
URL | url.searchParams.get("term") |
---|---|
/products?term=stretchy+pants |
"stretchy pants" |
/products?term= |
"" |
/products |
null |
¥Data Reloads
当渲染多个嵌套路由并且搜索参数发生变化时,所有路由都将重新加载(而不仅仅是新的或更改的路由)。这是因为搜索参数是一个跨字段关注点,可能会影响任何加载器。如果你希望在这种情况下防止某些路由重新加载,请使用 shouldRevalidate。
¥When multiple nested routes are rendering and the search params change, all the routes will be reloaded (instead of just the new or changed routes). This is because search params are a cross-cutting concern and could affect any loader. If you would like to prevent some of your routes from reloading in this scenario, use shouldRevalidate.
¥Search Params in Components
有时你需要从组件而不是加载器和操作中读取和更改搜索参数。根据你的用例,有多种方法可以实现这一点。
¥Sometimes you need to read and change the search params from your component instead of your loaders and actions. There are a handful of ways to do this depending on your use case.
设置搜索参数
¥Setting Search Params
设置搜索参数的最常见方法可能是让用户使用表单控制它们:
¥Perhaps the most common way to set search params is letting the user control them with a form:
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>
);
}
如果用户只选择了其中一个:
¥If the user only has one selected:
Nike
阿迪达斯
¥Adidas
然后 URL 将为 /products/shoes?brand=nike
¥Then the URL will be /products/shoes?brand=nike
如果用户同时选择了两个:
¥If the user has both selected:
Nike
阿迪达斯
¥Adidas
然后 URL 将为:/products/shoes?brand=nike&brand=adidas
¥Then the url will be: /products/shoes?brand=nike&brand=adidas
请注意,由于两个复选框都命名为 "brand"
,因此 brand
在 URL 搜索字符串中重复出现。在你的加载器中,你可以使用 searchParams.getAll
访问所有这些值。
¥Note that brand
is repeated in the URL search string since both checkboxes were named "brand"
. In your loader you can get access to all of those values with 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 }));
}
链接到搜索参数
¥Linking to Search Params
作为开发者,你可以通过链接到包含搜索字符串的 URL 来控制搜索参数。该链接将用链接中的内容替换 URL 中的当前搜索字符串(如果有):
¥As the developer, you can control the search params by linking to URLs with search strings in them. The link will replace the current search string in the URL (if there is one) with what is in the link:
<Link to="?brand=nike">Nike (only)</Link>
读取组件中的搜索参数
¥Reading Search Params in Components
除了在加载器中读取搜索参数外,你通常还需要在组件中访问它们:
¥In addition to reading search params in loaders, you often need access to them in components, too:
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
:
¥You might want to auto submit the form on any field change, for that there is 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>
);
}
命令式设置搜索参数
¥Setting Search Params Imperatively
虽然不常见,但你也可以随时出于任何原因强制设置 searchParams。这里的用例很少,少到我们甚至想不出一个好的例子,但这里有一个简单的例子:
¥While uncommon, you can also set searchParams imperatively at any time for any reason. The use cases here are slim, so slim we couldn't even come up with a good one, but here's a silly example:
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]);
// ...
}
¥Search Params and Controlled Inputs
你经常需要让某些输入项(例如复选框)与 URL 中的搜索参数保持同步。对于 React 的受控组件概念来说,这可能会有点棘手。
¥Often you want to keep some inputs, like checkboxes, in sync with the search params in the URL. This can get a little tricky with React's controlled component concept.
仅当搜索参数可以通过两种方式设置,并且我们希望输入与搜索参数保持同步时,才需要这样做。例如,<input type="checkbox">
和 Link
都可以更改此组件中的品牌:
¥This is only needed if the search params can be set in two ways, and we want the inputs to stay in sync with the search params. For example, both the <input type="checkbox">
and the Link
can change the brand in this component:
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
:
¥If the user clicks the checkbox and submits the form, the URL updates and the checkbox state changes too. But if the user clicks the link, only the url will update and not the checkbox. That's not what we want. You may be familiar with React's controlled components here and think to switch it to checked
instead of defaultChecked
:
<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
checked={brands.includes("adidas")}
/>
现在我们遇到了相反的问题:点击链接会同时更新 URL 和复选框的状态,但复选框不再起作用,因为 React 会阻止状态更改,直到控制它的 URL 发生更改 - 而它永远不会更改,因为我们无法更改复选框并重新提交表单。
¥Now we have the opposite problem: clicking the link updates both the URL and the checkbox state, but the checkbox no longer works because React prevents the state from changing until the URL that controls it changes--and it never will because we can't change the checkbox and resubmit the form.
React 希望你使用某些状态来控制它,但我们希望用户在提交表单之前控制它,然后我们希望 URL 在 URL 发生变化时控制它。因此,我们现在处于这个 "sorta-controlled" 的位置。
¥React wants you to control it with some state, but we want the user to control it until they submit the form, and then we want the URL to control it when it changes. So we're in this "sorta-controlled" spot.
你有两个选择,具体选择取决于你想要的用户体验。
¥You have two choices, and what you pick depends on the user experience you want.
首选:最简单的方法是在用户点击复选框时自动提交表单:
¥First Choice: The simplest thing is to auto-submit the form when the user clicks the checkbox:
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()
也自动提交,这样事件就不会冒泡到表单,否则每次点击复选框都会重复提交。)
¥(If you are also auto submitting on the form onChange
, make sure to e.stopPropagation()
so the event doesn't bubble up to the form, otherwise you'll get double submissions on every click of the checkbox.)
第二选择:如果你希望输入框为 "半控制",其中复选框反映 URL 状态,但用户也可以在提交表单和更改 URL 之前将其打开或关闭,则需要连接一些状态。这需要一些工作,但很简单:
¥Second Choice: If you want the input to be "semi controlled", where the checkbox reflects the URL state, but the user can also toggle it on and off before submitting the form and changing the URL, you'll need to wire up some state. It's a bit of work but straightforward:
根据搜索参数初始化一些状态
¥Initialize some state from the search params
当用户点击复选框时,更新状态,使复选框变为 "checked"
¥Update the state when the user clicks the checkbox, so the box changes to "checked"
当搜索参数发生变化(用户提交表单或点击链接)时,更新状态以反映 URL 搜索参数中的内容
¥Update the state when the search params change (the user submitted the form or clicked the link) to reflect what's in the url search params
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>
);
}
你可能想对复选框进行如下抽象:
¥You might want to make an abstraction for checkboxes like this:
<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
props 技巧重新安装它。虽然这种做法很巧妙,但这会导致可访问性问题,因为当用户点击节点后,React 会将其从文档中移除,从而导致用户失去焦点。
¥Option 3: We said there were only two options, but there is a third unholy option that might tempt you if you know React pretty well. You might want to blow away the input and remount it with key
prop shenanigans. While clever, this will cause accessibility issues as the user will lose focus when React removes the node from the document after they click it.
<input
type="checkbox"
id="adidas"
name="brand"
value="adidas"
key={"adidas" + brands.includes("adidas")}
defaultChecked={brands.includes("adidas")}
/>
¥Remix Optimizations
Remix 通过仅加载页面中导航时发生变化部分的数据来优化用户体验。例如,考虑你现在在这些文档中使用的 UI。侧面的导航栏位于父路由中,该路由获取所有文档的动态生成菜单,而子路由获取你正在阅读的文档。如果你点击侧边栏中的链接,Remix 会知道父级路由将保留在页面上 - 但子级路由的数据将会更改,因为文档的 url 参数会发生变化。使用此洞察后,Remix 将不会重新提取父路由的数据。
¥Remix optimizes the user experiences by only loading the data for the parts of the page that are changing on navigation. For example, consider the UI you're using right now in these docs. The navbar on the side is in a parent route that fetched the dynamically generated menu of all the docs, and the child route fetched the document you're reading right now. If you click a link in the sidebar, Remix knows that the parent route will remain on the page — but the child route's data will change because the url param for the document will change. With this insight, Remix will not refetch the parent route's data.
如果没有 Remix,下一个问题就是 "如何重新加载所有数据?"。它也内置于 Remix 中。每当调用 action(用户提交表单,或者你作为程序员从 useSubmit
调用 submit
)时,Remix 都会自动重新加载页面上的所有路由,以捕获可能发生的任何更改。
¥Without Remix, the next question is "how do I reload all the data?" This is built into Remix as well. Whenever an action is called (the user submitted a form or you, the programmer, called submit
from useSubmit
), Remix will automatically reload all the routes on the page to capture any changes that might have happened.
你无需担心缓存过期,也无需在用户与你的应用交互时避免过度获取数据,这一切都是自动补齐的。
¥You don't have to worry about expiring caches or avoid over-fetching data as the user interacts with your app, it's all automatic.
Remix 会在三种情况下重新加载所有路由:
¥There are three cases where Remix will reload all of your routes:
操作(表单、useSubmit
、fetcher.submit
)后
¥After an action (forms, useSubmit
, fetcher.submit
)
如果 URL 搜索参数发生变化(任何加载器都可以使用它们)
¥If the url search params change (any loader could use them)
用户点击指向他们当前所在 URL 的链接(这也会替换历史记录堆栈中的当前条目)
¥The user clicks a link to the exact same URL they are already at (this will also replace the current entry in the history stack)
所有这些行为都模拟了浏览器的默认行为。在这种情况下,Remix 对你的代码了解不够,无法优化数据加载,但你可以使用 shouldRevalidate 自行优化。
¥All of these behaviors emulate the browser's default behavior. In these cases, Remix doesn't know enough about your code to optimize the data loading, but you can optimize it yourself with shouldRevalidate.
¥Data Libraries
得益于 Remix 的数据约定和嵌套路由,你通常无需使用 React Query、SWR、Apollo、Relay、urql
等客户端数据库。如果你使用像 redux 这样的全局状态管理库,主要用于与服务器上的数据交互,那么你也不太可能需要它们。
¥Thanks to Remix's data conventions and nested routes, you'll usually find you don't need to reach for client side data libraries like React Query, SWR, Apollo, Relay, urql
and others. If you're using global state management libraries like redux, primarily for interacting with data on the server, it's also unlikely you'll need those.
当然,Remix 不会阻止你使用它们(除非它们需要 bundler 集成)。你可以引入任何你喜欢的 React 数据库,并在你认为比 Remix API 更能服务 UI 的任何地方使用它们。在某些情况下,你可以使用 Remix 进行初始服务器渲染,然后在后续交互中切换到你喜欢的库。
¥Of course, Remix doesn't prevent you from using them (unless they require bundler integration). You can bring whatever React data libraries you like and use them wherever you think they'll serve your UI better than the Remix APIs. In some cases you can use Remix for the initial server render and then switch over to your favorite library for the interactions afterward.
也就是说,如果你引入外部数据库并绕过 Remix 自身的数据约定,Remix 将无法再自动
¥That said, if you bring an external data library and sidestep Remix's own data conventions, Remix can no longer automatically
服务器渲染你的页面
¥Server render your pages
当 JavaScript 加载失败时,要能够灵活应对网络状况
¥Be resilient to network conditions when JavaScript fails to load
在用户与你的网站交互时进行优化,通过仅加载页面变化部分的数据来提高网站速度。
¥Make optimizations as the user interacts with your site to make it fast by only loading data for the changing parts of the page
在转换过程中并行获取数据、JavaScript 模块、CSS 和其他资源,避免渲染+获取瀑布流导致 UI 卡顿。
¥Fetch data, JavaScript modules, CSS, and other assets in parallel on transitions, avoiding render+fetch waterfalls that lead to choppy UI
通过在操作之后重新验证,确保 UI 中的数据与服务器上的数据同步
¥Ensure the data in the UI is in sync with the data on the server by revalidating after actions
出色的后退/前进滚动恢复(即使跨域)
¥Excellent scroll restoration on back/forward clicks (even across domains)
使用 错误边界 处理服务器端错误
¥Handle server-side errors with error boundaries
使用 错误边界 为 "未找到" 和 "未经授权" 启用可靠的用户体验
¥Enable solid UX for "Not Found" and "Unauthorized" with error boundaries
帮助你保持 UI 的良好运行。
¥Help you keep the happy path of your UI happy.
相反,你需要做额外的工作来提供良好的用户体验。
¥Instead, you'll need to do extra work to provide a good user experience.
Remix 旨在满足你设计的任何用户体验。虽然你意外地需要外部数据库,但你可能仍然需要一个,这没问题!
¥Remix is designed to meet any user experience you can design. While it's unexpected that you need an external data library, you might still want one and that's fine!
随着你学习 Remix,你会发现自己从客户端状态的思维方式转变为 URL 思维方式,并且这样做会让你免费获得很多东西。
¥As you learn Remix, you'll find you shift from thinking in client state to thinking in URLs, and you'll get a bunch of stuff for free when you do.
¥Gotchas
加载器仅在服务器上通过浏览器的 fetch
调用,因此你的数据会先使用 JSON.stringify
序列化并通过网络发送,然后再到达你的组件。这意味着你的数据需要可序列化。例如:
¥Loaders are only called on the server, via fetch
from the browser, so your data is serialized with JSON.stringify
and sent over the network before it makes it to your component. This means your data needs to be serializable. For example:
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"}'
}
并非所有内容都能成功!加载器用于处理数据,而数据需要可序列化。
¥Not everything makes it! Loaders are for data, and data needs to be serializable.
某些数据库(例如 FaunaDB)返回带有方法的对象,你需要在从加载器返回之前仔细序列化这些方法。通常这不是问题,但最好了解数据是通过网络传输的。
¥Some databases (like FaunaDB) return objects with methods that you'll want to be careful to serialize before returning from your loader. Usually this isn't a problem, but it's good to understand that your data is traveling over the network.
此外,Remix 会为你调用加载器;在任何情况下,你都不应尝试直接调用你的加载器:
¥Additionally, Remix will call your loaders for you; in no case should you ever try to call your loader directly:
export const loader = async () => {
return json(await fakeDb.products.findMany());
};
export default function RouteComp() {
const data = loader();
// ...
}