跳至主要內容
版本:v18.0.0

可重新提取的片段

在本節中,我們將研究如何根據使用者輸入提取不同的資料。

  • 我們將建構一個可篩選的朋友列表
  • 我們將了解如何僅重新提取必要的資料,而不是整個查詢。

由於 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>
);
}

您現在應該會在頂部看到一個包含人員列表的側邊欄。

Contacts list

看看 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
}
}
`;

請記住,此處的第一個 searchcontacts 的引數名稱,而第二個 $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。此 Hook 會傳回一個 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 為我們提供了一個重新提取的回呼,而不是接受新的狀態變數作為 Hook 的引數,並在以不同的值重新呈現時重新提取。這表示提取會在事件發生時立即開始,與等待 React 完成重新呈現相比,可節省一些時間 — 這與我們之前看到的預載查詢的原則相同。它還為我們提供了更多控制權,例如,如果我們想要延遲重新提取。

步驟 6 — 使用 useTransition 控制載入

此時,當片段重新整理時,Relay 會在載入新資料時使用 Suspense,因此整個元件會被旋轉器取代!這使得 UI 相當無法使用。我們寧願在有新資料可用之前,只將目前的資料保留在螢幕上。

Suspense 的正常運作方式如下:當元件缺少它需要呈現的資料時(就像我們在重新提取之後的元件一樣),它會告訴 React 等待。發生這種情況時,React 會在樹狀結構中找到最近的 Suspense 元件。然後,它會將該元件下的所有內容替換為「後援」載入指示器。

Component needs data React finds the nearest Suspense point Renders a fallback at that point until the data is available

這在最初載入畫面時是有意義的,但在這種情況下,沒有理由隱藏現有的 UI 並將其替換為旋轉器。在 React 等待時,它可以簡單地繼續顯示已有的內容。

為了實現這一點,我們可以將重新提取標記為轉場。轉場是不需要立即回應的 React 狀態更新 — React 可以等到資料可用。

轉場會透過將狀態變更包裝在 useTransition Hook 提供的函式呼叫中來標記。以下是程式碼的外觀

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 等待新資料時,React 不是使用 Suspense 後援,而是使用設為 true 的 isPending 旗標重新呈現元件。

我們只需將 isPending 旗標傳遞給 SearchInput(這會使其顯示旋轉器),同時重新提取正在進行中。同時,透過將 setSearchString 放置在轉場之外,但將 refetch 放置在其中,我們告訴 React 立即更新搜尋輸入。

我們現在應該能夠以良好的使用者體驗搜尋聯絡人列表,在載入時顯示微調器,同時保持先前的資料可見。

Search input goes from spinner to filtered list

深入探討:哪些片段可以重新擷取?

為了重新擷取片段,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 一起使用,您的 schema 必須宣告其類型實作一個名為 Node 的介面

type Person implements Node {
id: ID!
...
}

Node 介面只是表示它有一個 ID,但更重要的是,它按照約定表明該 ID 是穩定且唯一的

interface Node {
id: ID!
}

除了實作 Node 的類型上的片段之外,您還可以重新擷取 Viewer 上的片段(因為假定 viewer 在整個會話中都是穩定的),以及位於查詢頂層的片段(因為它們之上沒有任何欄位可以變更身分)。


總結

可重新擷取的片段讓我們能夠在回應使用者輸入時有效地更新 UI 的特定部分,同時將它們初始化為我們用於整個螢幕的相同查詢的一部分。

Relay 的分頁功能也建立在可重新擷取的片段之上。我們接下來將探索這些功能。