インタラクションのためのクエリ
フラグメントによって各コンポーネントのデータ要件を指定し、ランタイム時に1つのクエリで画面全体を実行する方法を見てきました。ここでは、同じ画面で2番目のクエリが必要な状況を見ていきます。これにより、GraphQLクエリのさらに多くの機能を探索することもできます。
- ユーザーが名前の上にカーソルを置くと、ストーリーの投稿者に関する詳細情報を表示する**ホバーカード**を作成します。
- ホバーカードは、ユーザーがホバーしたときのみ必要な**追加情報**を取得するために、2番目のクエリを使用します。
- **クエリ変数**を使用して、サーバーにどの人の詳細情報が必要かを伝えます。
- **プリロードされたクエリ**を使用してパフォーマンスを向上させる方法を示します。
これらのトピックを網羅した後、フラグメントのより高度な機能を見ていきます。
このセクションでは、PosterByline
にホバーカードを追加して、ユーザーが名前の上にカーソルを置くと、ストーリーの投稿者に関する詳細情報を表示できるようにします。
詳細:2番目のクエリを使用するタイミング
Relayは、画面全体のデータ要件を事前にすべて取得するのに役立つように設計されていることをすでに述べました。しかし、これは一般化して、ユーザーインタラクションは最大で1つのクエリを持つべきであると言うことができます。別の画面に移動することは、一般的なユーザーインタラクションの1つのタイプにすぎません。
画面内では、一部のインタラクションによって、最初に表示されたものから追加データが開示される場合があります。インタラクションが比較的まれに実行されるものの、大量の追加データが必要な場合は、画面が最初にロードされたときに事前にではなく、インタラクションが発生したときに実行される2番目のクエリで追加データを取得するのが賢明な場合があります。これにより、最初のロードが高速化され、コストが削減されます。
取得されるデータの量が不確定であるインタラクション(例:ホバーカード内のホバーカード)もあり、静的に知ることはできません。
データが低優先度で、メインデータのロード後にロードする必要があるが、ユーザーのさらなる入力なしで自動的に表示される必要がある場合、Relayには、そのための遅延フラグメントと呼ばれる機能があります。これは後で説明します。
すでに使用できるホバーカードコンポーネントを用意しています。ただし、ImageFragment
を使用するため、コンパイルエラーを回避するために、future
というディレクトリに配置されていました。チュートリアルのこの段階になったので、future
内のモジュールをsrc/components
に移動できます。
mv future/* src/components
これで、PosterByline
でフラグメントを使用する演習を行った場合、PosterByline
コンポーネントは次のようになります。
export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
return (
<div className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
</div>
);
}
ホバーカードコンポーネントを使用するには、次の変更を行います。
import Hovercard from './Hovercard';
import PosterDetailsHovercardContents from './PosterDetailsHovercardContents';
const {useRef} = React;
...
export default function PosterByline({ poster }: Props): React.ReactElement {
const data = useFragment(PosterBylineFragment, poster);
const hoverRef = useRef(null);
return (
<div
ref={hoverRef}
className="byline">
<Image image={data.profilePicture} width={60} height={60} className="byline__image" />
<div className="byline__name">{data.name}</div>
<Hovercard targetRef={hoverRef}>
<PosterDetailsHovercardContents />
</Hovercard>
</div>
);
}
誰かの名前の上にカーソルを置くと、詳細情報を含むホバーカードが表示されるようになりました。PosterDetailsHovercardContents.tsx
の中を見てみると、そのコンポーネントがマウントされたときに追加情報を取得するために、useLazyLoadQuery
で2番目のクエリを実行していることがわかります。
問題が1つあります。どの投稿者の上にカーソルを置いても、常に同じ人の情報が表示されます!
クエリ変数
サーバーに、どの情報の詳細情報が必要かを伝える必要があります。GraphQLを使用すると、特定のフィールドに引数として渡すことができるクエリ変数を定義できます。これらの引数は、サーバーで使用できます。
前のセクションでは、フィールドが引数をどのように受け入れることができるかを見ましたが、引数の値はハードコーディングされていました(例:url(width: 200, height: 200)
)。クエリ変数を使用すると、これらの値をランタイム時に決定できます。それらは、クエリ自体とともに、クライアントからサーバーに渡されます。GraphQL変数は常に$
ドル記号で始まります。
PosterDetailsHovercardContents.tsx
の中を見てください。このようなクエリが表示されます。
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
node
フィールドは、スキーマで定義されたトップレベルフィールドであり、一意のIDが与えられた任意のグラフノードを取得できます。これは、現在ハードコーディングされているIDを引数として受け入れます。この演習では、このハードコーディングされたIDを、UI状態によって提供される変数に置き換えます。奇妙に見える... on Actor
は型絞り込みです。これについては、次のセクションで詳しく説明しますが、今は無視できます。簡単に言うと、node
フィールドに任意のIDを指定できるため、静的にどの型のノードを選択しているかを判断できません。型絞り込みは期待する型を指定し、Actor
型のフィールドを使用できるようにします。
その中で、表示するフィールドを含むフラグメントを単純に展開します—後で詳しく説明します。今のところ、このハードコーディングされたIDをホバーしている投稿者のIDに置き換える手順を次に示します。
ステップ1:クエリ変数の定義
まず、クエリを編集して、クエリ変数を受け入れるように宣言する必要があります。変更点は次のとおりです。
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery(
$posterID: ID!
) {
node(id: "1") {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
- 変数名は
$posterID
です。これは、GraphQLクエリの残りの部分で、UIから渡される値を参照するために使用するシンボルです。 - 変数には型があります—この場合は
ID!
です。ID
型はString
の同義語であり、他の文字列と区別するためにノードIDに使用されます。ID!
の!
は、フィールドがnull許容ではないことを意味します。GraphQLでは、フィールドは通常null許容であり、null許容ではないことは例外です。
ステップ2:フィールド引数として変数を渡す
これで、ハードコーディングされた"1"
を新しい変数に置き換えます。
const PosterDetailsHovercardContentsQuery = graphql`
query PosterDetailsHovercardContentsQuery($posterID: ID!) {
node(
id: $posterID
) {
... on Actor {
...PosterDetailsHovercardContentsBodyFragment
}
}
}
`;
クエリ変数は、フィールド引数だけでなく、フラグメントの引数としても使用できます。
ステップ3:useLazyLoadQuery
に引数の値を提供する
これで、ランタイム時にUIから実際の値を渡す必要があります。useLazyLoadQuery
フックの2番目の引数は、変数値を含むオブジェクトです。コンポーネントに新しいプロップを追加して、その値を渡します。
export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}
ステップ4:親コンポーネントからIDを渡す
これで、ホバーカードの親コンポーネントであるPosterByline
からposterID
プロップを提供する必要があります。そのファイルに移動し、フラグメントにid
を追加して、IDをプロップとして渡します。
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
id
...
}
`;
export default function PosterByline({ poster }: Props): React.ReactElement {
...
return (
...
<PosterDetailsHovercardContents
posterID={data.id}
/>
...
);
}
この時点で、ホバーカードは、ホバーしている各投稿者に関する適切な情報を表示するはずです。
ブラウザのネットワークインスペクターを使用すると、変数値がクエリと共に渡されていることを確認できます。
また、このリクエストは、特定の投稿者に初めてカーソルを置いたときだけ行われることに気付くかもしれません。Relayはクエリの結果をキャッシュして再利用し、最近使用されていない場合は最終的にキャッシュされたデータを削除します。
詳細:キャッシュとRelayストア
他のほとんどのシステムとは異なり、Relayのキャッシュはクエリではなく、グラフノードに基づいています。Relayは、フェッチしたすべてのノードのローカルキャッシュをRelayストアとして保持します。ストア内の各ノードは、そのIDによって識別され、取得されます。ノードIDで識別されるように、2つのクエリが同じ情報を要求する場合、2番目のクエリは、最初のクエリで取得されたキャッシュされた情報を使用して満たされ、フェッチされません。このキャッシュ動作を利用するには、欠損フィールドハンドラーを構成してください。
Relayは、マウントされたコンポーネントによって使用されている、または最近使用されているクエリから「到達可能」でない場合、ストアからノードをガベージコレクションします。
詳細解説:GraphQLが変数のための構文を必要とする理由
GraphQLが変数の概念を持つ理由、つまり変数の値をクエリ文字列に直接挿入するのではなく、なぜそのような仕組みになっているのか疑問に思われるかもしれません。前に述べたように、GraphQLクエリ文字列のテキストは実行時に利用できません。Relayはそれをより効率的なデータ構造に置き換えるからです。コンパイラがビルド時に各クエリをサーバーにアップロードし、IDを割り当てるプリペアードクエリを使用するようにRelayを構成することもできます。その場合、実行時にRelayはサーバーに「クエリ#1337をください」と伝えるだけなので、文字列補間は不可能であり、そのため変数は帯域外で取得する必要があります。クエリ文字列が利用可能な場合でも、任意の値のシリアル化と文字列のエスケープに関する問題を回避するために、変数値を別々に渡すことで、HTTPリクエストに必要なもの以上の処理を行う必要がなくなります。
プリロードされたクエリ
このサンプルアプリは非常にシンプルなので、パフォーマンスは問題になりません。(実際、読み込み状態を認識できるように、サーバーは人工的に速度を落としています。)しかし、Relayの主な懸念事項の1つは、実アプリケーションで可能な限り高速なパフォーマンスを実現することです。
現時点では、ホバーカードはuseLazyLoadQuery
フックを使用しており、コンポーネントがレンダリングされるときにクエリをフェッチします。つまり、タイムラインは次のようになります。
理想的には、ネットワークフェッチはできるだけ早く開始する必要がありますが、ここではReactがレンダリングを完了するまで開始しません。このタイムラインは、インタラクションが発生したときにホバーカードコンポーネントのコード自体をロードするためにReact.lazy
を使用した場合は、さらに悪化する可能性があります。その場合、次のようになります。
GraphQLクエリのフェッチを開始する前に待機していることに注目してください。Reactコンポーネントがレンダリングされる前、マウスイベントハンドラーのまさに最初からクエリのフェッチを開始できればより良いでしょう。その場合、タイムラインは次のようになります。
ユーザーがインタラクションを行うと、コンポーネントのレンダリングを開始しながら(必要に応じてコードを最初にフェッチします)、必要なクエリのフェッチをすぐに開始する必要があります。これらの非同期処理の両方が完了したら、利用可能なデータを使用してコンポーネントをレンダリングし、ユーザーに表示できます。
Relayは、これを可能にするプリロードされたクエリという機能を提供しています。
ホバーカードをプリロードされたクエリを使用するように変更しましょう。
ステップ1 — useLazyLoadQuery
をusePreloadedQuery
に変更する
念のためですが、これは現在データを遅延読み込みしているPosterDetailsHovercardContents
コンポーネントです。
export default function PosterDetailsHovercardContents({
posterID,
}: {
posterID: string;
}): React.ReactElement {
const data = useLazyLoadQuery<QueryType>(
PosterDetailsHovercardContentsQuery,
{posterID},
);
return <PosterDetailsHovercardContentsBody poster={data.node} />;
}
これは、第2引数として変数を受け取るuseLazyLoadQuery
を呼び出します。これをusePreloadedQuery
に変更したいと考えています。しかし、プリロードされたクエリでは、変数は実際にはクエリがフェッチされるときに決定されます。これは、このコンポーネントがレンダリングされる前です。そのため、このフックは変数の代わりに、クエリの結果を取得するために必要な情報を含むクエリ参照を受け取ります。クエリ参照は、ステップ2でクエリをフェッチしたときに作成されます。
コンポーネントを次のように変更します。
import {usePreloadedQuery} from 'react-relay';
import type {PreloadedQuery} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as QueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
export default function PosterDetailsHovercardContents({
queryRef,
}: {
queryRef: PreloadedQuery<QueryType>,
}): React.ReactElement {
const data = usePreloadedQuery(
PosterDetailsHovercardContentsQuery,
queryRef,
);
...
}
ステップ2:親コンポーネントからアクセスできるようにクエリをエクスポートする
PosterDetailsHovercardContentsQuery
クエリを開始するために、親コンポーネントであるPosterByline
を変更します。そのためには、そのクエリへの参照が必要なので、エクスポートする必要があります。
export const PosterDetailsHovercardContentsQuery = graphql`...
ステップ3:親コンポーネントでuseQueryLoader
を呼び出す
PosterDetailsHovercardContents
がクエリ参照を期待するようになったので、そのクエリ参照を作成し、親コンポーネントであるPosterByline
から渡す必要があります。クエリ参照はuseQueryLoader
というフックを使用して作成します。このフックは、クエリフェッチをトリガーするためにイベントハンドラーで呼び出す関数も返します。
import {useQueryLoader} from 'react-relay';
import type {PosterDetailsHovercardContentsQuery as HovercardQueryType} from './__generated__/PosterDetailsHovercardContentsQuery.graphql';
import {PosterDetailsHovercardContentsQuery} from './PosterDetailsHovercardContents';
export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
return (
...
<PosterDetailsHovercardContents
queryRef={hovercardQueryRef}
/>
...
);
}
useQueryLoader
フックは、必要な2つのものを返します。
- クエリ参照は、
usePreloadedQuery
がクエリの結果を取得するために使用する不透明な情報です。 loadHovercardQuery
は、リクエストを開始する関数です。
ステップ4:イベントハンドラーでクエリをフェッチする
最後に、カードが表示されたときに発生するイベントハンドラーでloadHovercardQuery
を呼び出す必要があります。幸いにも、Hovercard
コンポーネントには、使用できるonBeginHover
イベントがあります。
export default function PosterByline({ poster }: Props): React.ReactElement {
...
const [
hovercardQueryRef,
loadHovercardQuery,
] = useQueryLoader<HovercardQueryType>(PosterDetailsHovercardContentsQuery);
function onBeginHover() {
loadHovercardQuery({posterID: data.id});
}
return (
<div className="byline">
...
<Hovercard
onBeginHover={onBeginHover}
targetRef={hoverRef}>
<PosterDetailsHovercardContents queryRef={hovercardQueryRef} />
</Hovercard>
</div>
);
}
リクエストを開始した場所に、クエリ変数が渡されることに注意してください。
この時点で、以前と同じ動作が見られるはずです。ただし、Relayがクエリをより早く開始できるため、少し高速になります。
簡潔にするためにuseLazyLoadQuery
を使用してクエリを紹介しましたが、プリロードされたクエリは、現実世界のパフォーマンスを大幅に向上させることができるため、常にRelayでクエリを使用する推奨方法です。サーバーとルーターシステムとの適切な統合を使用すると、クライアントコードをダウンロードまたは実行する前に、サーバー側でウェブページのメインクエリをプリロードすることさえできます。
概要
- 画面に最初に表示されるすべてのデータは1つのクエリにまとめる必要がありますが、追加の情報を必要とするユーザーインタラクションは、セカンダリクエリで処理できます。
- クエリ変数を使用すると、クエリと一緒にサーバーに情報を渡すことができます。
- クエリ変数は、フィールド引数に渡すことで使用されます。
- プリロードされたクエリは常に最良の方法です。ユーザーインタラクションクエリの場合は、イベントハンドラーでフェッチを開始します。画面の最初のクエリの場合は、特定のルーティングシステムでできるだけ早くフェッチを開始します。遅延読み込みクエリは、迅速なプロトタイピングの場合、またはまったく使用しない場合にのみ使用してください。
次に、さまざまなタイプのポスターを異なる方法で処理することでホバーカードを強化する方法を簡単に見ていきます。その後、初期クエリの対象となる情報も更新して、異なる変数で再フェッチする必要がある状況の処理方法を説明します。