再取得可能なフラグメント
このセクションでは、ユーザー入力に応じて異なるデータを取得する方法について説明します。
- フィルタ可能なフレンドリストを作成します。
- クエリ全体ではなく、必要なデータのみを再取得する方法を示します。
Relayでは、すべてのデータを1つの大きなクエリで取得することを推奨していますが、異なる変数で一部のデータを再取得する必要がある場合はどうすればよいでしょうか?
たとえば、フィルタ可能なリストを作成しているとします。検索入力の変更時に、新しい検索結果を取得する必要があります。
この問題への1つのアプローチは、リストを取得するために別のセカンダリクエリを使用することです。これは、前にホバーカードを取得するために使用した方法とよく似ています。次に、入力の変更時にクエリ変数を変更してクエリを再取得できます。
しかし、これは最適ではありません。ユーザー入力が発生する前に、最初のリストを取得するために、不必要に2番目のクエリを使用するためです。ホバーカードはユーザーインタラクションに応答してのみ表示されますが、フィルタ可能なリストが表示され、フィルタリングの準備ができている場合は、大きなクエリの1部としてその最初のコンテンツを取得しても構いません。
一方、入力が変更されるたびに大きなクエリ全体を再取得したくありません。これは、不必要に大量のデータを取得することを意味するだけでなく、UIの他の部分に支障をきたす可能性があります。フィルタ可能なリストに関連のない特定のデータがサーバーで変更された場合、クエリが再取得されるとランダムに変更されたように見えます。さらに、これはユーザー入力をクエリの存在するReactツリーの一番上にスレッド化することを意味し、これは非常にスケールしません。
これらの問題に対処するために、Relayは再取得可能なフラグメントを提供します。これらは、展開されているクエリの残りの部分とは別に、新しい変数で再取得できるフラグメントです。これにより、クエリの変数を変更して新しい引数値に対する新しいデータを取得できます。
しかし、フラグメントはまさにそれであり、フラグメントです。クエリではなく、クエリに展開され、クエリ結果から読み取られない限り取得できません。では、再取得可能なフラグメントは実際どのように機能するのでしょうか?答えは、Relayコンパイラがフラグメントを再取得するためだけに新しい個別のクエリを生成することです。データは、フラグメントが展開されているより大きなクエリの1部として最初に取得されますが、再取得されると、新しい合成クエリが使用されます。
これを試すには、フィルタ可能な連絡先リストを含むサイドバーをページに追加してみましょう。結局のところ、人々と連絡を取る機能がないと、適切な居心地の良いニュースフィードアプリとは言えません。
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
引数の新しい値を使用して、このフラグメントのみを再取得する検索入力を接続することです。
補足練習として、SidebarとNewsfeedのクエリを単一のクエリに結合してみてください。SidebarがNewsfeedとは別のクエリを持つ必要はありません。実際のアプリでは、両方ともフラグメントを持ち、画面全体は単一のクエリのみを持ちます。チュートリアルの初期の例を簡素化するために、別々のクエリで構築しました。
ステップ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
の引数の名前であり、2番目の$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が使用する規約です。Relayは、サーバーがIDを受け取り、そのIDを持つグラフノードを返す、node
と呼ばれるトップレベルのフィールドを実装することを期待しています。(hovercardの例でnode
を既に見てきました。そこでは、2番目のクエリを使用して特定のIDを持つ特定の人物を取得するために使用されました。)
すべてのグラフノードが安定したIDを持っているわけではありません。一部は一時的なものです。node
で使用するには、スキーマでその型がNode
というインターフェースを実装していることを宣言する必要があります。
type Person implements Node {
id: ID!
...
}
Node
インターフェースは、IDを持つことを単に述べているだけですが、より重要なのは、そのIDが安定していて一意であることを規約によって示していることです。
interface Node {
id: ID!
}
Node
を実装する型上のフラグメントに加えて、Viewer
上のフラグメント(ビューアはセッション全体で安定していると仮定されるため)と、クエリの一番上にあり(変更される可能性のあるフィールドが上にはないため)、再取得することもできます。
概要
再取得可能なフラグメントを使用すると、ユーザー入力に応じてUIの特定の部分を効率的に更新しながら、画面全体に使用するのと同じクエリの一部として初期化できます。
Relayのページネーション機能も、再取得可能なフラグメントに基づいています。次はそれらを詳しく見ていきましょう。