可重新获取的片段
在本节中,我们将介绍如何根据用户输入获取不同的数据。
- 我们将构建一个可过滤的朋友列表。
- 我们将了解如何仅重新获取必要的数据,而不是整个查询。
由于 Relay 鼓励您在一个大型查询中获取所有数据,那么当您需要使用不同的变量重新获取一些数据时会发生什么?
例如,假设您正在构建一个可过滤的列表。当搜索输入发生更改时,您需要获取新的搜索结果。
一种方法是使用单独的辅助查询来获取列表,就像我们之前获取悬停卡一样。然后,当输入发生更改时,我们可以更改查询变量并重新获取查询。
但是,这并不理想,因为它在任何用户输入发生之前,不必要地使用第二个查询来获取初始列表。悬停卡仅在响应用户交互时出现,但如果可过滤列表可见并已准备好进行过滤,那么我们不妨将它的初始内容作为我们大型查询的一部分。
另一方面,我们不希望在输入发生更改时重新获取整个大型查询。这不仅意味着不必要地检索大量数据,而且可能会破坏 UI 的其他部分。如果与可过滤列表无关的某些数据在服务器上发生了更改,那么在重新获取查询时,它似乎会随机更改。此外,这意味着将用户输入传递到查询所在的 React 树的顶部,这将难以扩展。
为了解决这些问题,Relay 提供了可重新获取的片段。这些片段可以单独使用新变量重新获取,而无需重新获取它们被展开到的查询的其余部分。它们允许我们更改片段的参数并获取针对新参数值的新的数据,就像我们可以使用新的查询变量获取整个查询一样。
但片段就是片段——它们不是查询,无法在没有被展开到查询并从查询结果中读取的情况下获取。那么可重新获取的片段是如何工作的呢?答案是 Relay 编译器会生成一个新的、单独的查询,只用于重新获取该片段。数据最初作为片段展开到的任何大型查询的一部分被检索,但随后在重新获取它时,会使用新的合成查询。
为了尝试一下,让我们在页面上添加一个边栏,其中包含一个可过滤的联系人列表。毕竟,如果没有与人联系的能力,它就不像一个舒适的新闻提要应用程序。
我们已经准备了一个Sidebar
组件,您只需将其放到App.tsx
中
import Sidebar from './Sidebar';
export default function App(): React.ReactElement {
return (
<RelayEnvironment>
<React.Suspense fallback={<LoadingSpinner />}>
<div className="app">
<Newsfeed />
<Sidebar />
</div>
</React.Suspense>
</RelayEnvironment>
);
}
您现在应该会看到一个带有联系人列表的边栏。
看看ContactsList.tsx
,您会发现这个片段,它就是选择联系人列表的片段
const ContactsListFragment = graphql`
fragment ContactsListFragment on Viewer {
contacts {
id
...ContactRowFragment
}
}
`;
碰巧的是,contacts
字段接受一个search
参数,它会过滤列表。您可以尝试将此片段中的contacts
更改为contacts(search: "S")
。如果您运行npm run relay
并刷新页面,您应该只看到那些包含字母 S 的联系人。
因此,我们的目标是连接一个搜索输入,以便当输入发生更改时,我们仅使用search
参数的新值来重新获取该片段。
作为一项可选练习,尝试将边栏和新闻提要的查询合并成一个查询。边栏不需要拥有与新闻提要分离的自己的查询;在实际应用中,它们都会有片段,并且整个屏幕只会拥有一个查询。我们创建了单独的查询来简化教程中的早期示例。
步骤 1 — 添加片段参数
首先,我们需要让这个片段接受一个参数。对于可重新获取的片段,片段参数会变成 Relay 生成的重新获取查询的查询变量。(它们也像常规片段参数一样工作,因此父查询可以为参数传递初始值。)
const ContactsListFragment = graphql`
fragment ContactsListFragment on Viewer
@argumentDefinitions(
search: {type: "String", defaultValue: null}
)
{
contacts {
id
...ContactRowFragment
}
}
`;
步骤 2 — 将片段参数作为字段参数传递
将片段参数作为contacts
字段的参数传递。
const ContactsListFragment = graphql`
fragment ContactsListFragment on Viewer
@argumentDefinitions(
search: {type: "String", defaultValue: null}
)
{
contacts(search: $search) {
id
...ContactRowFragment
}
}
`;
请记住,这里第一个search
是contacts
的参数名称,而第二个$search
是我们片段参数创建的变量。
步骤 3 — 添加@refetchable 指令
接下来,我们将添加一个@refetchable
指令。这会告诉 Relay 生成用于重新获取它的额外查询。您必须指定生成的查询的名称——最好根据片段的名称来命名。
const ContactsListFragment = graphql`
fragment ContactsListFragment on Viewer
@refetchable(queryName: "ContactsListRefetchQuery")
@argumentDefinitions(
search: {type: "String", defaultValue: null}
)
{
// ...
}
`;
步骤 4 — 添加搜索输入
现在,我们需要将其实际连接到我们的 UI。看看ContactsList
组件
export default function ContactsList({ viewer }: Props) {
const data = useFragment(ContactsListFragment, viewer);
return (
<Card dim={true}>
<h3>Contacts</h3>
{data.contacts.map(contact =>
<ContactRow key={contact.id} contact={contact} />
)}
</Card>
);
}
首先,我们需要添加一个搜索字段。
import SearchInput from './SearchInput';
const {useState} = React;
function ContactsList({viewer}) {
const data = useFragment(ContactsListFragment, viewer);
const [searchString, setSearchString] = useState('');
const onSearchStringChanged = (value: string) => {
setSearchString(value);
};
return (
<Card dim={true}>
<h3>Contacts</h3>
<SearchInput
value={searchString}
onChange={onSearchStringChanged}
/>
{data.contacts.map(contact =>
<ContactRow key={contact.id} contact={contact} />
)}
</Card>
);
}
步骤 5 — 调用useRefetchableFragment
现在,为了在字符串发生更改时重新获取片段,我们将useFragment
更改为useRefetchableFragment
。此钩子会返回一个refetch
函数,它会使用我们作为参数提供的新的变量来重新获取片段。
import {useRefetchableFragment} from 'react-relay';
function ContactsList({viewer}) {
const [data, refetch] = useRefetchableFragment(ContactsListFragment, viewer);
const [searchString, setSearchString] = useState('');
const onSearchStringChanged = (value) => {
setSearchString(value);
refetch({search: value});
};
return (
// ...
);
}
您会注意到,Relay 为我们提供了一个用于重新获取的回调,而不是将新的状态变量作为钩子的参数,并在它使用不同的值重新渲染时进行重新获取。这意味着获取会在事件发生后立即开始,与等待 React 完成重新渲染相比节省了一些时间——与我们之前在预加载查询中看到的一样。它还给了我们更多的控制权,例如,如果我们想对重新获取进行防抖。
步骤 6 — 使用useTransition控制加载
在这一点上,当片段被刷新时,Relay 会在加载新数据时使用 Suspense,因此整个组件会被一个加载指示器替换!这使得 UI 非常难以使用。我们宁愿在新的数据可用之前将当前数据保留在屏幕上。
Suspense 通常的工作方式是这样的:当一个组件缺少渲染所需的数据(就像我们在重新获取后一样),它会告诉 React 等待。当发生这种情况时,React 会在树中找到最近的 Suspense 组件。然后,它会将该组件下的所有内容替换为一个“回退”加载指示器。
这在最初加载屏幕时是有意义的,但在这种情况下,没有理由隐藏现有的 UI 并用一个加载指示器替换它。在 React 等待的同时,它可以简单地继续显示已经存在的内容。
为了实现这一点,我们可以将重新获取标记为一个过渡。过渡是 React 状态更新,不需要立即响应——React 可以等到数据可用再进行响应。
过渡通过将状态更改包装在对useTransition
钩子提供的函数的调用中来标记。这就是代码的样子
const {useState, useTransition} = React;
function ContactsList({viewer}) {
const [isPending, startTransition] = useTransition();
const [searchString, setSearchString] = useState('');
const [data, refetch] = useRefetchableFragment(ContactsListFragment, viewer);
const onSearchStringChanged = (value) => {
setSearchString(value);
startTransition(() => {
refetch({search: value});
});
};
return (
<Card dim={true}>
<h3>Contacts</h3>
<SearchInput
value={searchString}
onChange={onSearchStringChanged}
isPending={isPending}
/>
{data.contacts.map(contact =>
<ContactRow key={contact.id} contact={contact} />
)}
</Card>
);
}
在 React 等待新的数据时,它不会使用 Suspense 回退,而是会用isPending
标志设置为 true 的组件重新渲染组件。
我们只需将isPending
标志传递给SearchInput
(它会导致在重新获取发生时显示一个加载指示器),同时,通过将setSearchString
放置在过渡之外,而将refetch
放置在过渡内部,我们告诉 React 立即更新搜索输入。
我们现在应该能够使用良好的用户体验来搜索联系人列表,显示一个加载指示器,但在加载过程中保留以前可见的数据。
深入探讨:哪些片段可以重新获取?
为了重新获取片段,Relay 必须知道如何生成一个查询,使它能够重新获取片段中的信息。这只有在满足某些要求的片段中才有可能。
您可能想象,我们至少可以重新运行片段展开到的原始查询。但是,GraphQL 并不保证同一个查询在不同时间会返回相同的结果。例如,想象一下,您有一个返回网站上最热门帖子的 GraphQL 字段
query MyQuery {
topTrendingPosts {
title
summary
date
poster {
...PosterFragment
}
}
}
如果您想从这个查询中刷新PosterFragment
,它将无法像这样构建查询
query MyQuery {
topTrendingPosts {
poster {
...PosterFragment
}
}
}
...因为当您刷新它时,最热门的帖子可能是不同的帖子!
Relay 需要一种方法来识别图中片段最终到达的特定节点,即使它不再可以通过原始查询使用的相同路径到达。如果该节点具有唯一且稳定的 ID,那么我们就可以使用一个约定来查询“具有特定 ID 的图节点”,如下所示
query RefetchQuery {
node(id: "abcdef") {
...PosterFragment
}
}
事实上,这正是 Relay 使用的约定。它期望您的服务器实现一个名为node
的顶级字段,它接受一个 ID 并为您提供具有该 ID 的图节点。(我们之前在悬停卡示例中看到了node
——在那里,它被用来使用辅助查询来获取给定其 ID 的特定人。)
并非每个图节点都有一个稳定的 ID——有些是短暂的。为了与node
一起使用,您的模式必须声明它的类型实现了一个名为Node
的接口
type Person implements Node {
id: ID!
...
}
Node
接口只是说它有一个 ID,但更重要的是,它通过约定表明该 ID 是稳定的和唯一的
interface Node {
id: ID!
}
除了针对实现 Node
接口类型的片段,您还可以重新获取位于 Viewer
上的片段(因为假设 Viewer 在整个会话期间保持稳定),并且这些片段位于查询的顶层(因为没有高于它们的字段可以更改标识)。
摘要
可重新获取的片段使我们能够在用户输入时高效地更新 UI 的特定部分,同时将它们初始化为与我们用于整个屏幕的相同查询的一部分。
Relay 的分页功能也是基于可重新获取的片段构建的。我们将在接下来探索这些功能。