フラグメント
フラグメントは、Relayの特徴の1つです。フラグメントを使用すると、各コンポーネントが独自のデータニーズを独立して宣言できますが、単一のクエリの効率を維持できます。このセクションでは、クエリをフラグメントに分割する方法を示します。
まず、Storyコンポーネントにストーリーが投稿された日付を表示したいとします。それを行うには、サーバーからさらにデータが必要になるため、クエリにフィールドを追加する必要があります。
Newsfeed.tsx
に移動し、新しいフィールドを追加できるように NewsfeedQuery
を見つけてください。
const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
title
summary
createdAt // Add this line
poster {
name
profilePicture {
url
}
}
image {
url
}
}
}
`;
クエリを更新したので、npm run relay
を実行して、更新されたGraphQLクエリを認識するようにRelayコンパイラーを実行する必要があります。
次に、Story.tsx
に移動して日付を表示するように変更します。
import Timestamp from './Timestamp';
type Props = {
story: {
createdAt: string; // Add this line
...
};
};
export default function Story({story}: Props) {
return (
<Card>
<PosterByline poster={story.poster} />
<Heading>{story.title}</Heading>
<Timestamp time={story.createdAt} /> // Add this line
<Image image={story.image} />
<StorySummary summary={story.summary} />
</Card>
);
}
日付が表示されるようになりました。また、GraphQLのおかげで、新しいサーバーコードを作成してデプロイする必要はありませんでした。
しかし、考えてみると、なぜ Newsfeed.tsx
を変更する必要があったのでしょうか?Reactコンポーネントは自己完結型であるべきではないでしょうか?なぜNewsfeedがStoryに必要な特定のデータに関心を持つ必要があるのでしょうか?データが階層構造のはるか下にあるStoryの子コンポーネントで必要になった場合はどうでしょうか?多くの異なる場所で使用されているコンポーネントの場合はどうでしょうか?その場合、データ要件が変更されるたびに、多くのコンポーネントを変更する必要があります。
これらの多くの問題を回避するために、Storyコンポーネントのデータ要件を Story.tsx
に移動できます。
これを行うには、Story
のデータ要件を Story.tsx
で定義された*フラグメント*に分割します。フラグメントは、Relayコンパイラーが完全なクエリにまとめて結合するGraphQLの個別の部分です。フラグメントを使用すると、各コンポーネントが独自のデータ要件を定義でき、各コンポーネントが独自のクエリを実行することによるランタイムでのコストを払う必要がありません。
では、今すぐ Story
のデータ要件をフラグメントに分割しましょう。
ステップ1 — フラグメントを定義する
Story.tsx
(src/components
内) の Story
コンポーネントの上に以下を追加します。
import { graphql } from 'relay-runtime';
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
createdAt
poster {
name
profilePicture {
url
}
}
image {
url
}
}
`;
クエリ内の topStory
からすべての選択をコピーして、この新しいフラグメント宣言にコピーしたことに注意してください。クエリと同様に、フラグメントには名前 (StoryFragment
) があり、これは後で使用しますが、フラグメントが「オン」になっているGraphQL型 (Story
) もあります。これは、グラフ内にStoryノードがある場合はいつでもこのフラグメントを使用できることを意味します。
ステップ2 — フラグメントをスプレッドする
Newsfeed.tsx
に移動し、NewsfeedQuery
を次のように変更します。
const NewsfeedQuery = graphql`
query NewsfeedQuery {
topStory {
...StoryFragment
}
}
`;
topStory
内の選択を StoryFragment
に置き換えました。Relayコンパイラーは、Newsfeed
を変更しなくても、Storyのすべてのデータが今後フェッチされるようにします。
ステップ3 — useFragmentを呼び出す
Storyが空のカードをレンダリングするようになったことに気づくでしょう。すべてのデータがありません! Relayは、useLazyLoadQuery()
から取得したstory
オブジェクトにフラグメントで選択されたフィールドを含めることになっていなかったのでしょうか?
その理由は、Relayがそれらを隠しているからです。コンポーネントが特定のフラグメントのデータを明示的に要求しない限り、そのデータはコンポーネントに表示されません。これは*データマスキング*と呼ばれ、コンポーネントが他のコンポーネントのデータ依存関係に暗黙的に依存するのではなく、独自のフラグメント内で依存関係をすべて宣言することを強制します。これにより、コンポーネントは自己完結型で保守可能になります。
データマスキングがないと、他のコンポーネントがどこかでそれを使用していないことを検証するのが難しいため、フラグメントからフィールドを削除することはできません。
フラグメントによって選択されたデータにアクセスするには、useFragment
というフックを使用します。Story
を次のように変更します。
import { useFragment } from 'react-relay';
export default function Story({story}: Props) {
const data = useFragment(
StoryFragment,
story,
);
return (
<Card>
<Heading>{data.title}</Heading>
<PosterByline poster={data.poster} />
<Timestamp time={data.createdAt} />
<Image image={data.image} />
<StorySummary summary={data.summary} />
</Card>
);
}
useFragment
は2つの引数を受け取ります。
- 読み取りたいフラグメントのGraphQLタグ付き文字列リテラル
- フラグメントをスプレッドしたGraphQLクエリ内の場所から来る、以前に使用したのと同じstoryオブジェクト。これは、*フラグメントキー*と呼ばれます。
これにより、そのフラグメントによって選択されたデータが返されます。
ここでは、JSX内のすべてでstory
をdata
(useFragment
によって返されるデータ)に書き換えました。コンポーネントのコピーでも同じことを確認してください。そうしないと機能しません。
フラグメントキーは、フラグメントがスプレッドされたGraphQLクエリのレスポンス内の場所です。たとえば、Newsfeedクエリが与えられた場合
query NewsfeedQuery {
topStory {
...StoryFragment
}
}
queryResult
がuseLazyLoadQuery
によって返されるオブジェクトである場合、queryResult.topStory
はStoryFragment
のフラグメントキーになります。
技術的には、queryResult.topStory
は、RelayのuseFragment
に必要なデータの場所を伝えるいくつかの非表示フィールドを含むオブジェクトです。フラグメントキーは、読み取るノード(ここではストーリーが1つだけですが、すぐに複数のストーリーが表示されます)と、読み取ることができるフィールド(その特定のフラグメントで選択されたフィールド)の両方を指定します。次に、useFragment
フックは、Relayのローカルデータストアからその特定の情報を読み取ります。
後の例で説明するように、同じ場所に複数のフラグメントをクエリにスプレッドし、フラグメントスプレッドを直接選択されたフィールドと混在させることもできます。
ステップ4 — フラグメント参照のTypeScript型
フラグメント化を完了するには、TypeScriptがこのコンポーネントが生データをではなくフラグメントキーを受け取ることを期待することを認識するように、Props
の型定義も変更する必要があります。
クエリ(または別のフラグメント)にフラグメントをスプレッドすると、フラグメントをスプレッドした場所に対応するクエリ結果の部分が、そのフラグメントの*フラグメントキー*になることを思い出してください。これは、グラフ内のフラグメントを読み取る特定の場所を提供するために、propsでコンポーネントに渡すオブジェクトです。
これをタイプセーフにするために、Relayは、その特定のフラグメントのフラグメントキーを表す型を生成します。これにより、フラグメントをクエリにスプレッドせずにコンポーネントを使用しようとすると、型システムを満たすフラグメントキーを提供できなくなります。以下に示す変更が必要になります。
import type {StoryFragment$key} from './__generated__/StoryFragment.graphql';
type Props = {
story: StoryFragment$key;
};
これにより、Story
に必要なデータが何であるかを気にする必要がなくなり、独自のクエリ内でそのデータを事前にフェッチできるNewsfeed
が作成されました。
練習
Story
で使用されるPosterByline
コンポーネントは、投稿者の名前とプロフィール写真を表示します。これらの同じ手順を使用して、PosterByline
をフラグメント化します。以下を行う必要があります。
Actor
にPosterBylineFragment
を宣言し、必要なフィールド(name
、profilePicture
)を指定します。Actor
型は、ストーリーを投稿できる個人または組織を表します。StoryFragment
のposter
内でそのフラグメントをスプレッドします。useFragment
を呼び出してデータを取得します。PosterBylineFragment$key
をposter
プロップとして受け入れるようにPropsを更新します。
フラグメントの使用のメカニズムを理解するために、これらの手順をもう一度実行する価値があります。ここには、正しい方法で一緒にスロットインする必要がある多くの部分があります。
それが完了したら、フラグメントがアプリのスケーリングにどのように役立つかの基本的な例を見てみましょう。
複数の場所でフラグメントを再利用する
フラグメントは、特定の型の*いくつか*のグラフノードが与えられた場合、そのノードから読み取るデータを指定します。フラグメントキーは、グラフ内のデータが選択される*ノード*を指定します。フラグメントを指定する再利用可能なコンポーネントは、異なるフラグメントキーを渡すことにより、異なるコンテキストでグラフの異なる部分からデータを取得できます。
たとえば、Image
コンポーネントは、ストーリーのサムネイル画像としてStory
内で直接使用され、投稿者のプロフィール写真としてPosterByline
内でも使用されていることに注意してください。Image
をフラグメント化し、使用場所に応じてグラフ内の異なる場所から必要なデータをどのように選択できるかを見てみましょう。
ステップ1 — フラグメントを定義する
Image.tsx
を開いて、フラグメント定義を追加します。
import { graphql } from 'relay-runtime';
const ImageFragment = graphql`
fragment ImageFragment on Image {
url
}
`;
ステップ2 — フラグメントをスプレッドする
StoryFragment
とPosterBylineFragment
に戻り、Image
コンポーネントがデータを使用している各場所でImageFragment
をスプレッドします。
- Story.tsx
- PosterByline.tsx
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment
}
}
`;
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
name
profilePicture {
...ImageFragment
}
}
`;
ステップ 3 — useFragment の呼び出し
Image
コンポーネントを、そのフラグメントを使ってフィールドを読み取るように変更し、フラグメントキーを受け入れるように Props も変更します。
import { useFragment } from 'react-relay';
import type { ImageFragment$key } from "./__generated__/ImageFragment.graphql";
type Props = {
image: ImageFragment$key;
...
};
function Image({image}: Props) {
const data = useFragment(ImageFragment, image);
return <img key={data.url} src={data.url} ... />
}
ステップ 4 — 一度修正すれば、どこでも利用可能
Image
のデータ要件をフラグメント化し、コンポーネント内に配置したことで、それを使用するコンポーネントを変更せずに、Image
に新しいデータ依存関係を追加できるようになりました。
たとえば、アクセシビリティのために Image
コンポーネントに altText
ラベルを追加してみましょう。
ImageFragment
を次のように編集します。
const ImageFragment = graphql`
fragment ImageFragment on Image {
url
altText
}
`;
これで、Story、Newsfeed、その他のコンポーネントを編集することなく、クエリ内のすべての画像に代替テキストがフェッチされるようになります。したがって、新しいフィールドを使用するように Image
を変更するだけで済みます。
function Image({image}) {
// ...
<img
alt={data.altText}
//...
}
これで、ストーリーのサムネイル画像とポスターのプロフィール写真の両方に代替テキストが付くようになります。(ブラウザの要素インスペクターを使用して確認できます。)
コードベースが大きくなるにつれて、これがどれほど有益になるか想像できるでしょう。コンポーネントが使用される場所の数に関係なく、各コンポーネントは自己完結型です!コンポーネントが何百もの場所で使用されている場合でも、そのデータ依存関係からフィールドを自由に追加または削除できます。これは、Relay がアプリの規模に合わせてスケーリングするのに役立つ主な方法の 1 つです。
フラグメントは Relay アプリの構成要素です。そのため、多くの Relay 機能はフラグメントに基づいています。次のセクションで、そのいくつかを見ていきましょう。
フラグメント引数とフィールド引数
現在、Image
コンポーネントは、小さいサイズで表示される場合でも、フルサイズの画像をフェッチしています。これは非効率的です!Image
コンポーネントは、画像を表示するサイズを伝える prop を受け取るため、Image
を使用するコンポーネントによって制御されます。同様の方法で、Image
を使用するコンポーネントが、そのフラグメント内でフェッチする画像のサイズを指定できるようにしたいと考えます。
GraphQL フィールドは、サーバーがリクエストを完了するための追加情報を提供する引数を受け入れることができます。たとえば、Image
型の url
フィールドは、サーバーが URL に組み込む height
および width
引数を受け入れます。このフラグメントがある場合
fragment Example1 on Image {
url
}
/images/abcde.jpeg
のような URL を取得する可能性があります。
一方、このフラグメントがある場合
fragment Example2 on Image {
url(height: 100, width: 100)
}
/images/abcde.jpeg?height=100&width=100
のような URL を取得する可能性があります。
もちろん、ImageFragment
に特定のサイズをハードコーディングしたくはありません。なぜなら、Image
コンポーネントがさまざまなコンテキストで異なるサイズをフェッチできるようにしたいからです。そのためには、ImageFragment
がフラグメント引数を受け入れるようにすることで、親コンポーネントがフェッチする必要のある画像のサイズを指定できるようにします。これらのフラグメント引数は、特定のフィールド (この場合は url
) にフィールド引数として渡すことができます。
それを行うには、ImageFragment
を次のように編集します。
const ImageFragment = graphql`
fragment ImageFragment on Image
@argumentDefinitions(
width: {
type: "Int",
defaultValue: null
}
height: {
type: "Int",
defaultValue: null
}
)
{
url(
width: $width,
height: $height
)
altText
}
`;
これを分解してみましょう。
- フラグメント宣言に
@argumentDefinitions
ディレクティブを追加しました。これは、フラグメントが受け入れる引数を指定します。各引数について、次のものを指定します。- 引数の名前
- その型 (任意の GraphQL スカラ型 にすることができます)
- オプションで デフォルト値 — この場合、デフォルト値は null であり、固有のサイズの画像をフェッチできます。デフォルト値が指定されていない場合、フラグメントが使用されるすべての場所で引数が必要です。
- 次に、フラグメント引数を変数として使用して、GraphQL フィールドに引数を設定します。ここでは、フィールド引数とフラグメント引数が同じ名前になっています (多くの場合そうですが)。ただし、
width:
はフィールド引数であり、$width
はフラグメント引数によって作成された変数であることに注意してください。
これで、フラグメントは、選択したフィールドの 1 つを介してサーバーに渡す引数を受け入れます。
深掘り: GraphQL ディレクティブ
フラグメント引数の構文は、かなり煩雑に見えるかもしれません。これは、GraphQL 言語を拡張するためのシステムであるディレクティブに基づいているためです。GraphQL では、@
で始まる記号はすべてディレクティブです。その意味は GraphQL 仕様で定義されていませんが、特定のクライアントまたはサーバーの実装に委ねられています。
Relay は、その機能をサポートするために いくつかのディレクティブ を定義しています。その 1 つがフラグメント引数です。これらのディレクティブはサーバーに送信されませんが、ビルド時に Relay コンパイラに指示を与えます。
GraphQL 仕様では、実際には 3 つのディレクティブの意味を定義しています。
@deprecated
はスキーマ定義で使用され、フィールドを非推奨としてマークします。@include
および@skip
は、フィールドの包含を条件付きにするために使用できます。
これらに加えて、GraphQL サーバーは、スキーマの一部として追加のディレクティブを指定できます。また、Relay には独自のビルド時ディレクティブがあり、これにより、文法を変更することなく言語を少し拡張できます。
ステップ 2
これで、Image
を使用するさまざまなフラグメントが、各画像に適切なサイズを渡すことができます。
- Story.tsx
- PosterByline.tsx
const StoryFragment = graphql`
fragment StoryFragment on Story {
title
summary
postedAt
poster {
...PosterBylineFragment
}
thumbnail {
...ImageFragment @arguments(width: 400)
}
}
`;
const PosterBylineFragment = graphql`
fragment PosterBylineFragment on Actor {
name
profilePicture {
...ImageFragment @arguments(width: 60, height: 60)
}
}
`;
アプリがダウンロードする画像を見ると、それらがより小さなサイズになっていることがわかります。これにより、ネットワーク帯域幅が節約されます。フラグメント引数の値に整数リテラルを使用しましたが、後のセクションで説明するように、実行時に提供される変数を使用することもできます。
フィールド引数 (例: url(height: 100)
) は GraphQL 自体の機能であり、フラグメント引数 (@argumentDefinitions
および @arguments
) は Relay 固有の機能です。Relay コンパイラは、フラグメントをクエリに結合するときに、これらのフラグメント引数を処理します。
まとめ
フラグメントは、Relay が GraphQL を使用する上で最も特徴的な側面です。データを表示し、そのデータのセマンティクスを気にしているすべてのコンポーネント (単なるタイポグラフィックまたはフォーマット コンポーネントではない) で、GraphQL フラグメントを使用してデータ依存関係を宣言することをお勧めします。
- フラグメントはスケーリングに役立ちます。コンポーネントが使用される場所の数に関係なく、1 つの場所でデータ依存関係を更新できます。
- フラグメントデータは
useFragment
で読み取る必要があります。 useFragment
は、グラフのどこから読み取るかを指定するフラグメントキーを受け取ります。- フラグメントキーは、そのフラグメントが spread された GraphQL 応答内の場所から取得されます。
- フラグメントは、spread される時点で使用される引数を定義できます。これにより、使用される各状況に合わせて調整できます。
クエリ全体を再フェッチせずに、単一のフラグメントの内容を再フェッチする方法など、フラグメントの他の多くの機能についても後で検討します。ただし、その前に、配列について学習して、このニュースフィードアプリをよりニュースフィードらしくしてみましょう。