可重新提取的片段
在本節中,我們將研究如何根據使用者輸入提取不同的資料。
- 我們將建構一個可篩選的朋友列表。
- 我們將了解如何僅重新提取必要的資料,而不是整個查詢。
由於 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
。此 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 元件。然後,它會將該元件下的所有內容替換為「後援」載入指示器。
這在最初載入畫面時是有意義的,但在這種情況下,沒有理由隱藏現有的 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 立即更新搜尋輸入。
我們現在應該能夠以良好的使用者體驗搜尋聯絡人列表,在載入時顯示微調器,同時保持先前的資料可見。
深入探討:哪些片段可以重新擷取?
為了重新擷取片段,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 的分頁功能也建立在可重新擷取的片段之上。我們接下來將探索這些功能。