一覧へ

Relay : あなたのために泥臭い仕事をしてくれるGraphQLクライアント

私はGraphQLクライアントのRelayのファンで、2年ほど前から使用しています。 Relayは現代のフロントエンドの開発抱える課題とGraphQLへの深い理解に裏打ちされたFacebook渾身のフレームワークですが、残念ながらその哲学は十分に理解されていなかったように思います。

そして2020年。ついにRelayをうまく表現した文章がやってきました。本記事はその翻訳になります 1。 普段は異なるGraphQLクライアントを使っている方でも、Relayの哲学はコンポーネント指向のフロントエンド開発者なら刺激的で参考になると思います。ぜひご覧ください。

(原文)


Relay入門シリーズ(全4記事)
Relay : あなたのために泥臭い仕事をしてくれるGraphQLクライアント(本記事)
GraphQLでのConnectionベースのページネーション
Relayを使って最小限の努力でページネーションをする
Nodeインターフェースの魔法

この連載は、Gabriel NordebornSean Groveが執筆しています。GabrielはスウェーデンのITコンサルタント会社Arizonのフロントエンド開発者でありパートナーでもあり、長い間Relayを利用してきました。SeanはサードパーティのAPIをGraphQLで統一するOneGraph.comの共同創業者です。

この連載では、ひとつの疑問に明確に答えるために、Relayを深く掘り下げていきたいと思います。

「Relay(Facebookが提供するGraphQLを使ったアプリケーションを構築するためのJavaScriptクライアントフレームワーク)になぜ私が興味を持つのか?」

これは良い疑問です。その疑問に答えるために、ブログをレンダリングする簡単なページを構築するところから見ていきましょう。ページを構築していくと、2つの主要なテーマが浮かび上がってきます。

  1. Relayは実際、あなたのために望んで泥臭い仕事をする働きものです
  2. Relayが提示する規則に従えば、GraphQLを使ってクライアントサイドのアプリケーションを構築する際に素晴らしい開発者体験を提供してくれます

また、Relayを使ったアプリケーションがスケーラブルで、パフォーマンスが高く、モジュール化されており、何もせずともデフォルトで変更に強く、そしては現在開発中のReactの新機能に対応した将来性のあるものになることもお見せします。

Relayには(若干の)コストがかかりますが、これについては正直に前もって検討しますので、トレードオフについては十分に理解していただけます。

この記事について

この記事ではRelayの考え方や哲学を紹介することを目的としています。時折、他の GraphQLフレームワークとRelayの動作を比較することはありますが、この記事は Relayと他のフレームワークを比較すること自体を目的にしていません。Relayの哲学やアプリケーション構築に関わる概念を説明し、Relayそのものについて深く掘り下げていきたいと考えています。

この記事に掲載しているコードサンプル(いくつかあります!)は、あくまでもRelayの動作を説明するためのものであり、少し浅く単純化されているかもしれません。

また、ReactのSuspenseとConcurrent Modeに完全対応した、Relayの新しいHooksベースのAPIにも注目します。新しいAPIはまだexperimental 2ですが、FacebookはRelayとデータレイヤー専用のAPIを使ってfacebook.comを再構築しています 3

始める前に、この記事ではGraphQLの基本的な知識とクライアントサイドのJavaScriptアプリケーションの構築について基本的な知識があることを前提としています。GraphQLについては、まだよくわかっていないという方はこちらの記事で紹介しています。コードサンプルはTypeScriptで書かれているので、Typescriptの基本的な理解があればそれも役立つでしょう。

最後に、この記事はかなり長いです。何度も読み返すことができる参考記事として参照してください。

ここまでの点をふまえて、さっそく始めましょう!

Relayの概要

Relayは、GraphQLコードを最適化するコンパイラと、Reactで使用するライブラリで構成されています。

深く飛び込む前に、まずはRelayの概要を簡単に説明しましょう。Relayは2つの部分に分けることができます。

  1. コンパイラ:あらゆる種類の最適化、型の生成、そして優れた開発者体験を可能にする役割を担っています。Relayを使って開発している時はコンパイラはバックグラウンドで動作します。
  2. ライブラリ:Relayのコアと、ReactでRelayを使用するためのバインディング。

この時点でコンパイラについて知っておくべきことは、あなたが起動する別プロセスですべてのGraphQLのoperation 4を監視してコンパイルするということだけです。コンパイラの詳細についてはすぐに説明します。

加えて、Relayが最適に動作するためには、スキーマが3つの規則に従うことが必要です。

  • GraphQLの型のすべてのidフィールドはグローバルに一意であること(つまり、異なる種類のオブジェクトであっても、2つのオブジェクトは同じidの値を持ってはいけません)。
  • Nodeインターフェースが必要です。これが意味するのは、グラフ内のオブジェクトはトップレベルのnodeフィールドでidフィールドを指定して取得できるべきということです。グローバルに一意なidNodeインターフェースについての詳細は、こちらの記事 を参照してください。
  • ページネーションは、コネクションベースのページネーション規則に従うべきです。コネクションベースのページネーションとは何か、なぜそれが良いアイデアなのかについては、こちらの記事 を読んでください。

この時点では規則についてはこれ以上深く掘り下げませんが、興味のある方は上記のリンク先の記事をチェックすることをお勧めします。

Relayの核心: fragment

まず最初に、RelayとGraphQLの統合の核となるコンセプトについてお話しましょう。それがfragmentです。fragmentは、Relay(とGraphQL!)の力を発揮するための主要なキーの一つです。

簡単に言えば、GraphQLのfragmentとは、特定のGraphQLの型(type)で共通の項目をグループ化する方法のことです。以下に例を示します。

fragment Avatar_user on User {
  avatarUrl
  firstName
  lastName
}

興味のある方のために: fragmentの名前に Avatar_user という名前を付けることは、Relayが強制している規則です。Relayでは、すべてのfragmentの名前はグローバルに一意で、<moduleName>_<propertyName>の構造に従うようにします。fragmentの命名規則についてはこちらをご覧ください。

これはGraphQLの型であるUserで使用できるAvatar_userという名前のfragmentを定義しています。このfragmentはアバターをレンダリングするのに必要なフィールドを指定します。このfragmentを使うことで、アバターのレンダリングに必要なフィールドを必要な場所ごとで明示的に指定するのではなく、query全体でこのfragmentを再利用することができます。

# authorのアバターとlikedByで使用するアバーターで別々に定義する代わりに…
query BlogPostQuery($blogPostId: ID!) {
  blogPostById(id: $blogPostId) {
    author {
      firstName
      lastName
      avatarUrl
    }
    likedBy(first: 2) {
      edges {
        node {
          firstName
          lastName
          avatarUrl
        }
      }
    }
  }
}

# こうやってAvatar_userでまとめることができる
query BlogPostQuery($blogPostId: ID!) {
  blogPostById(id: $blogPostId) {
    author {
      ...Avatar_user
    }
    likedBy(first: 2) {
      edges {
        node {
          ...Avatar_user
        }
      }
    }
  }
}

こうすると定義を再利用できるので便利ですが、より重要なのは、アプリケーションの進化に合わせてアバターをレンダリングするのに必要なフィールドを一箇所で追加したり削除したりできることです。

fragmentを使用すると、GraphQL 型のフィールドの再利用可能な指定を定義することができます。

Relayはfragmentをもっと推し進める

GraphQLクライアントアプリケーションを長期的にスケールさせするためには、データをレンダリングするコンポーネントを必要なデータと一緒に配置する(コロケーション)ことが良い方法です。そうすることで、コンポーネントのメンテナンスや拡張が非常に簡単になります。

fragmentでは(上記で説明したように)特定のGraphQLの型のフィールドのサブフィールド 5を定義することができるので、コロケーションの考え方にぴったりです。

コンポーネントではレンダリングする必要のあるデータを記述した1つ以上のfragmentを定義するのが良い方法です。これは、コンポーネントが「親コンポーネントが誰であるかに関係なく、User型のこれら3つのフィールドに依存している」と言うことができることを意味します。上の例では、<Avatar />という名前のコンポーネントがあり、Avatar_user fragmentで定義されたフィールドを使ってアバターを表示します。

ほとんどのフレームワークでは、何らかの方法で GraphQL fragmentを使用することができます。しかし、Relayはこれをさらに進化させています。Relayでは、ほとんどすべてがfragmentを中心に展開されています。

RelayはどうやってGraphQL fragmentを展開するのか

根本的に、Relayはすべてのコンポーネントに対して、コンポーネントと共に全てのデータの要件を完全かつ明示的にリストアップさせたいと考えています。 これによりRelayはfragmentと深く統合することが可能になります。これが何を意味し、何を可能にするのかを分解してみましょう。

コロケーションされたデータ要件とモジュール性

Relayでは、fragmentを使用してコンポーネントのデータ要件を実際に使用しているコードのすぐそばに配置します。Relayの規則に従うことで、すべてのコンポーネントがアクセスを必要とするすべてのフィールドを明示的にリストアップすることが保証されます。つまり、明示的に要求されていないデータに依存するコンポーネントは存在しないということです。コンポーネントはモジュール化されており、自己完結型であり、再利用やリファクタリングに強くなります。

Relayでは、fragmentを使ってモジュール化を可能にするためのさまざまな機能も提供していますが、それについてはこの記事の後半で少し触れます。

パフォーマンス

Relayでは、コンポーネントは使用しているフィールドが変更された場合にのみ再レンダリングを行います。あなたが何かをする必要はありません! これは各fragmentが指定したデータの更新のみを受け取るためです。

Relayはデフォルトでビューの更新方法を最適化しているため、アプリの成長に伴ってパフォーマンスが不必要に低下することはありません。これは他のGraphQLクライアントの動作とは全く異なります。以下では、これがスケーラビリティにとってどれほど重要であるかについて、いくつかの素晴らしい例を紹介します。

以上のことを念頭に置いて、いよいよページの構築に取り掛かりましょう

Relayはfragmentの概念を推し進め、データ要件のコロケーション、モジュール性、優れたパフォーマンスを可能にするために利用しています。

ブログ記事をレンダリングするページの構築

これは、1つのブログ記事を表示するページがどのように見えるかを説明したワイヤーフレームです。 画像1.png まず、このビューのすべてのデータを1つのトップレベルのqueryで取得する方法を考えてみましょう。ワイヤーフレームのニーズを満たすための合理的なqueryは、次のようになります。

query BlogPostQuery($blogPostId: ID!) {
  blogPostById(id: $blogPostId) {
    author {
      firstName
      lastName
      avatarUrl
      shortBio
    }
    title
    coverImgUrl
    createdAt
    tags {
      slug
      shortName
    }
    body
    likedByMe
    likedBy(first: 2) {
      totalCount
      edges {
        node {
          firstName
          lastName
          avatarUrl
        }
      }
    }
  }
}

1つのqueryで必要なデータをすべて取得することができます! 良いですね!

そして、UIコンポーネントの構造は以下のようになるかもしれません。

<BlogPost>
  <BlogPostHeader>
    <BlogPostAuthor>
      <Avatar />
    </BlogPostAuthor>
  </BlogPostHeader>
  <BlogPostBody>
    <BlogPostTitle />
    <BlogPostMeta>
      <CreatedAtDisplayer />
      <TagsDisplayer />
    </BlogPostMeta>
    <BlogPostContent />
    <LikeButton>
      <LikedByDisplayer />
    </LikeButton>
  </BlogPostBody>
</BlogPost>

これをRelayで構築する方法を見てみましょう。

Relayでのデータの問い合わせ

Relayでは、ブログ記事をレンダリングするルートコンポーネントは通常以下のように書きます。

// BlogPost.ts
import * as React from 'react';
import { useLazyLoadQuery } from 'react-relay/hooks';
import { graphql } from 'react-relay';
import { BlogPostQuery } from './__generated__/BlogPostQuery.graphql';
import { BlogPostHeader } from './BlogPostHeader';
import { BlogPostBody } from './BlogPostBody';

interface Props {
  blogPostId: string;
}

export const BlogPost = ({ blogPostId }: Props) => {
  const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
    graphql`
      query BlogPostQuery($blogPostId: ID!) {
        blogPostById(id: $blogPostId) {
          ...BlogPostHeader_blogPost
          ...BlogPostBody_blogPost
        }
      }
    `,
    {
      variables: { blogPostId },
    },
  );

  if (!blogPostById) {
    return null;
  }

  return (
    <div>
      <BlogPostHeader blogPost={blogPostById} />
      <BlogPostBody blogPost={blogPostById} />
    </div>
  );
};

ここで何が起こっているのか、一歩一歩分解してみましょう。

const { blogPostById } = useLazyLoadQuery<BlogPostQuery>(
    graphql`
      query BlogPostQuery($blogPostId: ID!) {
        blogPostById(id: $blogPostId) {
          ...BlogPostHeader_blogPost
          ...BlogPostBody_blogPost
        }
      }
    `,
    {
      variables: { blogPostId },
    },
  );

まず注意したいのは、次のようにReactのhookであるuseLazyLoadQueryをRelayから利用することです。 const { blogPostById } = useLazyLoadQuery<BlogPostQuery> useLazyLoadQueryは、コンポーネントがレンダリングされるとすぐにBlogPostQueryの取得を開始します。

型の安全性を確保するために、useLazyLoadQueryに型を指定します。./__generated__/BlogPostQuery.graphqlからインポートしたBlogPostQueryという型を明示的に指定しています。このファイルはRelayコンパイラによって自動的に生成され(queryの定義を変更すると同期して保存されます)、queryに必要なすべての型情報(戻ってくるデータがどのように見えるか、queryが必要とする変数は何かなど)を持っています。

留意点!:前述したように、useLazyLoadQueryは、queryがレンダリングされるとすぐにqueryの取得を開始します。しかし、Relayは実際には、このようにレンダリング時にデータを遅延ロードで取得することを望んでいません。むしろ、ページのレンダリングと同時にではなく、ユーザーが新しいページへのリンクをクリックしたときなど、できるだけ早くqueryの読み込みを開始してほしいと考えています。なぜこれが重要なのかについては、こちらのブログ記事トークで詳しく説明しています。 この記事では、ほとんどの人にとってより身近なメンタルモデルであることと、できるだけシンプルで簡単に理解できるようにするために、まだ遅延ロードを使用しています。しかし、前述したようにこれは実際にRelayを使って構築する際にqueryでデータを取得する方法ではないことに注意してください。

次に、実際のqueryを見てみましょう。

    graphql`
      query BlogPostQuery($blogPostId: ID!) {
        blogPostById(id: $blogPostId) {
          ...BlogPostHeader_blogPost
          ...BlogPostBody_blogPost
        }
      }
    `

このqueryにはほとんど解説する部分はありません。queryにはブログ記事をIDで指定する以外に、あと2つの項目があります。BlogPost<BlogPostHeader /><BlogPostBody />のfragmentです。

使用しているfragmentをインポートする必要がないことに注意してください。これらはRelayコンパイラによって自動的にインポートされます。これについては後ほど詳しく説明します。

このようにfragmentを組み合わせてqueryを構築することは非常に重要です。別のアプローチとしては、コンポーネントごとに独自のqueryを定義させて、独自にデータを取得する責任を持たせるという方法もあります。これにはいくつかの有効なユースケースがありますが、2つの大きな問題があります。

  • 1つのqueryではなく、大量のqueryがサーバに送られてきます。
  • queryを作成する各コンポーネントは、実際にレンダリングされてからデータの取得を開始するまで待たなければなりません。つまり、リクエストはウォーターフォールで行われるため、ビューの読み込みが必要以上に遅くなる可能性が高いということです。

Relayでは、コンポーネントを組み合わせてUIを構築します。これらのコンポーネントでは、他のコンポーネントがどのようなデータを必要としているのか見えないように定義します。

Relayでモジュール性をどのように実現するか

上のコードで覚えておくべきメンタルモデルは以下の通りです。

BlogPostコンポーネントは2つの子コンポーネント、BlogPostHeaderBlogPostBodyをレンダリングしたいことだけを知っています。2つのコンポーネントがどのようなデータを必要としているのかは知りません(親コンポーネントが知る必要はありませんよね。それは子コンポーネントのやるべきことです!)。 代わりに、必要なすべてのデータはGraphQLのBlogPost型のBlogPostHeader_blogPostおよびBlogPostBody_blogPostと呼ばれるfragmentに含まれていると子コンポーネントは教えてくれます。これらのfragmentをqueryに含める限り、詳細がわからなくても、必要なデータを確実に取得できることがわかっています。そして、必要なデータを手に入れたら、それらをレンダリングすることができます。

私たちは、独自のデータ要件を定義するコンポーネントを独立して構成することで UI を構築します。これらのコンポーネントは、独自のデータ要件を持つ他のコンポーネントと一緒に構成することができます。 しかし、どのコンポーネントも他のコンポーネントがどのようなデータを必要としているかについては、そのコンポーネントがどのGraphQLのデータソース(型)からデータを必要としているか以外は何も知りません。Relayは、泥臭い作業を処理して、適切なコンポーネントが適切なデータを取得するようにしたり、サーバに送信されるqueryで必要なデータが一括ですべて指定さるようにします。

これにより、開発者はコンポーネントfragmentを独立して考えることができ、Relayが全てを繋ぎ合わせる作業を担当してくれます。

それでは次のステップへ行きましょう!

Relayコンパイラは、あなたのプロジェクトで定義したすべてのGraphQLコードを把握している

先ほどのqueryでは2つのfragmentを参照していましたが、それらのfragmentがどのファイルのどこで定義されているかを指定したり、queryに手動でインポートしたりする必要はありませんでした。これは、Relayでは各fragmentにグローバルに一意な名前を付与しているため、Relayコンパイラはそれらを見つけてサーバに送信するqueryにfragmentの定義を自動的に含めることができるからです。

手作業でfragmentの定義を参照することは、不便で、手動で、エラーが発生しやすい作業ですが、Relayでは開発者の責任ではありません。

コンポーネントと密接に結合されたfragmentを使用することで、Relayはコンポーネントのデータ要件を外部から隠すことができ、モジュール性と安全なリファクタリングが可能になります。

最後に、結果をレンダリングする部分を見てみましょう。

// 両方のフラグメントをblogPostByIdに展開させているので、`BlogPostHeader` と `BlogPostBody` の両方のコンポーネントの要件を満たすことが保証されています。
  if (!blogPostById) {
    return null;
  }

  return (
    <div>
      <BlogPostHeader blogPost={blogPostById} />
      <BlogPostBody blogPost={blogPostById} />
    </div>
  );

ここでは、<BlogPostHeader /><BlogPostBody />をレンダリングしています。よく見ると、両方とも blogPostByIdオブジェクトを渡してレンダリングしていることがわかるかもしれません。これは、fragmentを展開した 6 query内のオブジェクトです。これがRelayでのfragmentデータの転送方法です。fragmentを使用して、コンポーネントにfragmentを展開したオジェクトを渡し、コンポーネントはそれを使って実際のfragmentデータを取得します。 心配しないでください、投げっぱなしにはしません。型システムを通じて、Relayは正しいfragmentが展開された正しいオブジェクトを確実に渡します。これについてはもう少し詳しく説明します。

ふー、新しいことがいくつかありましたね。しかし、これまでに見てきたところでは、Relayが私たちを助けるために行っていることの多くを紹介してきましたが、これらのメリットを受けるためには通常は手動で行わなければならないことばかりです。

Relayの規則に従うことで、要求されたデータを持っていないとコンポーネントをレンダリングすることができません。つまり、壊れたコードをプロダクションに出荷するのに苦労することになります。

それでは、コンポーネントのツリーを下に移動していきましょう。

fragmentを使ったコンポーネントの構築

<BlogPostHeader>のコードを示します。

// BlogPostHeader.ts
import * as React from 'react';
import { useFragment } from 'react-relay/hooks';
import { graphql } from 'react-relay';
import { 
  BlogPostHeader_blogPost$key,
  BlogPostHeader_blogPost
} from './__generated__/BlogPostHeader_blogPost.graphql';
import { BlogPostAuthor } from './BlogPostAuthor';
import { BlogPostLikeControls } from './BlogPostLikeControls';

interface Props {
  blogPost: BlogPostHeader_blogPost$key;
}

export const BlogPostHeader = ({ blogPost }: Props) => {
  const blogPostData = useFragment<BlogPostHeader_blogPost>(
    graphql`
      fragment BlogPostHeader_blogPost on BlogPost {
        title
        coverImgUrl
        ...BlogPostAuthor_blogPost
        ...BlogPostLikeControls_blogPost
      }
    `,
    blogPost,
  );

  return (
    <div>
      <img src={blogPostData.coverImgUrl} />
      <h1>{blogPostData.title}</h1>
      <BlogPostAuthor blogPost={blogPostData} />
      <BlogPostLikeControls blogPost={blogPostData} />
    </div>
  );
};

ここでの例では、1つのコンポーネントにつき1つのfragmentしか定義していませんが、1つのコンポーネントは、同じタイプの複数のfragmentを含む任意の数の GraphQLの型で任意の数のfragmentを定義することができます。

分解してみましょう。

import { 
  BlogPostHeader_blogPost$key,
  BlogPostHeader_blogPost
} from './__generated__/BlogPostHeader_blogPost.graphql';

Relay コンパイラが自動生成した BlogPostHeader_blogPost.graphqlファイルから2つの型定義をインポートします。

Relayコンパイラはファイル 7からGraphQL fragmentコードを抽出し、型定義を生成します。プロジェクトで作成してRelayで使用するすべてのGraphQLコード(query、mutation、subscription、fragment)に対してこの処理を行います。これは、fragmentの定義が変更されてもコンパイラが自動的に型を同期させることを意味します。

BlogPostHeader_blogPostにはfragmentの型定義が含まれており、それをuseFragment (後ほど詳しく説明します)に渡すことで、fragmentのデータとの対話が型安全になるようにしています。

しかし、12行目のinterface Props { … }にあるBlogPostHeader_blogPost$keyは一体何なのでしょうか?まあ、それは型の安全性に関係しています。本当に今は気にする必要はありませんが、好奇心旺盛な方のためにとりあえず分解してみましょう(そうでない方は次の見出しに飛ばしてください)。

この型定義は、型の黒魔法を使って、正しいオブジェクト(BlogPostHeader_blogPostfragmentが展開されるべき場所)だけをuseFragmentに渡すことができるようにし、間違っている場合はビルド時にエラーを発生させます(エラーをエディタで確認できます!)。ご覧のように、propsからblogPostを受け取りuseFragmentの2番目の引数として渡しています。そして、もしblogPostが正しいfragment(BlogPostHeader_blogPost)を展開していない場合、型エラーが発生します。

全く同じデータを指定した別々のfragmentが同時にそのオブジェクトに展開されていても問題ありません。RelayはuseFragmentを使って、使用したいfragmentが正確に正しいかどうかを確認してくれます。これは重要なことです。他のコンポーネントに暗黙のうちに影響を受けることなく、fragmentの定義を変更できることをRelayが保証する方法の一つだからです。

Relayは正しいフラグメントを含んでいる正しいオブジェクトを渡すことで、間違ったデータソースを参照するようなの潜在的なエラーの原因を排除します。

明示的に指定したデータだけが使用することができる

fragmentとしてBlogPostHeader_blogPostBlogPost型で定義しました。このコンポーネントには2つのフィールドを明示的に指定しています。

- `title`
- `coverImgUrl`

これは、このコンポーネントでこれらのフィールドを使用しているからです。このことは、Relayの重要な機能のひとつであるデータのマスキングを強調しています。次の展開したfragment 8であるBlogPostAuthor_blogPosttitlecoverImgUrlを指定していたとしても 、自分のfragmentで明示的に要求しない限り、これらのフィールドにアクセスすることはできません(つまり、フィールドは取得の定義を書いた場所でのみ使用されなければなりません)。

これは型レベル(生成された型に明示的に指定されなかったフィールドは含まれません)でも実行時にも適用されます 。仮に型システムをバイパスしても、値は存在しません。

最初は少し奇妙に感じるかもしれませんが、これもRelayの安全機構のひとつです。他のコンポーネントが指定したデータに暗黙的に依存することが不可能であることがわかっていれば、他のコンポーネントが奇妙で予期せぬ方法で壊れるリスクを冒すことなく、コンポーネントのリファクタリングを行うことができます。これはアプリが成長していく上で非常に有効です。また、すべてのコンポーネントとそのデータ要件が完全に自己完結するようになります。

コンポーネントが必要とするすべてのデータが明示的に定義されていることは、他のコンポーネントが依存していたqueryやfragmentからフィールドの指定を削除することで、誤ってUIを壊してしまうことがないことを意味します。

  const blogPostData = useFragment<BlogPostHeader_blogPost>(
    graphql`
      fragment BlogPostHeader_blogPost on BlogPost {
        title
        coverImgUrl
        ...BlogPostAuthor_blogPost
        ...BlogPostLikeControls_blogPost
      }
    `,
    blogPost,
  );

ここでは、React hookのuseFragmentを使ってfragmentのデータを取得しています。useFragmentは、フラグメントの定義graphqlタグの中で定義されたもの)と、そのフラグメントが展開されたオブジェクト(ここではpropsからきたblogPost)を引数から理解し、それを使ってこの特定のフラグメントのデータを取得します。

再度確認しますが、このfragmentのデータ(title/coverImgUrl)は、propsからきたblogPostからは直接取得できません。 データは、fragmentの定義とfragmentが展開されたオブジェクトであるblogPostを用いて、useFragmentを呼び出すことでのみ取得できます。

そして、先ほどと同じように、レンダリングしたいコンポーネントのfragmentを展開します。この場合、BlogPostAuthor_blogPostBlogPostLikeControls_blogPostです。

好奇心のある方へ:fragmentはどのフィールドを指定するかを記述するだけなので、useFragmentは実際にデータをGraphQLのAPIにリクエストすることはありません。fragmentのデータを取得するためには、どこかの時点でquery (または他のGraphQL operation) を実行しなければなりません。しかし、Relayには非常にクールな機能があり、fragmentのデータをRelay自身が再取得できるようになっています。これは、特定の GraphQL オブジェクトをidで再取得するためのqueryをRelayが自動的に生成してくれるからです。話は脱線してしまいましたが…。

また、Redux をご存知であれば、useFragmentをステートツリーから必要なものだけを取得するselectorに例えることもできます。

  return (
    <div>
      <img src={blogPostData.coverImgUrl} />
      <h1>{blogPostData.title}</h1>
      <BlogPostAuthor blogPost={blogPostData} />
      <BlogPostLikeControls blogPost={blogPostData} />
    </div>
  );

明示的に要求したデータ(coverImgUrltitle)をレンダリングし、2つの子コンポーネントにデータを渡して、子コンポーネントがレンダリングできるようにします。fragmentを展開するコンポーネントにオブジェクトを渡すことに注意してください。このオブジェクトはfragmentのルートであるBlogPostHeader_blogPostで定義され使用されます。

どうやってRelayはパフォーマンスを維持するのか

fragmentを使用する場合、各fragmentは実際に使用しているデータの更新のみをsubscribeします。つまり、上記の<BlogPostHeader />コンポーネントは、レンダリングしている特定のブログ記事のcoverImgUrltitleが更新された場合にのみ、自分自身で再レンダリングを行います。もしBlogPostAuthor_blogPostが他のフィールドを指定し、それらのフィールドが更新された場合でも、このコンポーネントは再レンダリングされません。データの変更はfragmentレベルでsubscribeされます。

これは少し混乱するように聞こえるかもしれませんし、最初はそれほど便利ではないかもしれませんが、パフォーマンスのためには非常に重要なことです。ここでは、クライアントでGraphQLデータを扱う場合の一般的な方法と対比させて、この点について詳しく見ていきましょう。

Relayでは、データが更新されると、更新されたデータを使用しているコンポーネントのみが再レンダリングされます。

ビューで使うデータはどこから来るのか? 他のフレームワークとの比較

ビューで使用するすべてのデータは、queryのようにサーバーから実際にデータを取得する操作に由来していなければなりません。queryを定義し、フレームワークにサーバーからそれを取得させ、必要なデータを渡して、ビュー内で必要なコンポーネントをレンダリングします。ほとんどの GraphQLフレームワークでは、データのソースはqueryです。データはqueryからコンポーネントへと流れていきます。他の GraphQL フレームワークで一般的にどのように行われているかの例を示します(矢印はデータがどのように流れるかを表しています)。 画像2.png

注: フレームワークのデータストアは、多くのフレームワークではキャッシュと呼ばれています。この記事では、フレームワークのデータストア === キャッシュと仮定します。

フローは以下のようになります。

  1. <Profile />query ProfileQueryを作成し、GraphQL APIにリクエストを発行します。
  2. レスポンスはフレームワーク固有のデータストア (キャッシュ) に何らかの方法で保存されます。
  3. データはレンダリングのためにビューに届けられます。
  4. ビューはその後、データの一部を必要とする子孫コンポーネント(AvatarNameBioなど)に渡し続けます。最後に、あなたのビューはレンダリングされます。

Relayはどのように行うのか

さて、Relayの場合はこれとは全く異なります。Relayの場合はイラストがどうなっているのか見てみましょう。 画像3.png

何が違うのでしょうか?

  • queryが GraphQL API に発行され、データはフレームワークのデータストアに格納されます。ここまでは同じですが、ここから違いが出てきます。
  • データを使用するすべてのコンポーネントは、データストア(キャッシュ)から直接データを取得していることに注目してください。これは、Relayのfragmentとの深い統合によるものです。UIでは、各fragmentはフレームワークのデータストアから直接データを取得し、実際のデータがqueryから渡されることには依存しません。
  • queryから他のコンポーネントの矢印は消えています。queryから一部の情報は、データストアから必要なデータを探すためにfragmentに渡されています。しかし、実際のデータはfragmentに渡されず、全ての実際のデータはfragment自身によってデータストアから取得されます。

以上、Relayやその他のGraphQLフレームワークの仕組みについて、詳しく説明してきました。なぜこれにこだわる必要があるのでしょうか? 実は、この設定によりいくつかの優れた機能が利用できるようになります。

他のフレームワークでは通常、データのソースとしてqueryを使用し、そのデータを他のコンポーネントにツリーダウンして渡すことに頼っています。Relayではこれを逆転させ、各コンポーネントがデータストアから必要なデータを取得できるようにします。 9

タダでパフォーマンスが得られる

考えてみてください。queryがデータのソースである場合、queryが持っているデータに影響を与えるデータストアへの更新は、queryを保持しているコンポーネントの再レンダリングを強制します。つまり、データストアの更新によって再レンダリングが発生し、子コンポーネントに渡すために親コンポーネントからデータを取得する以外に、更新とは何の関係もないコンポーネントのいくつものレイヤーを経由してカスケードしなければなりません。

各コンポーネントが必要なデータをストアから直接取得し、使用するデータのみを対象に更新をsubscribeするというRelayのアプローチでは、アプリのサイズや複雑さが大きくなってもパフォーマンスを維持することができます。

これはsubscriptionを使用する際にも重要です。Relayでは、subscriptionから送られてくる更新データが、その更新データを実際に使用しているコンポーネントの再レンダリングのみを発生させるようにしています。

データのソースとしてqueryを使用すると、GraphQLのキャッシュが更新されたときにコンポーネントツリー全体が再レンダリングされることになります。

モジュール化と独立性は、安全にリファクタリングできることを意味する

queryからデータを実際に必要とするコンポーネントにデータをルーティングする責任を開発者から取り除くことで、開発者が物事を台無しにする機会がなくなります。queryのデータにアクセスできないということは、コンポーネントツリーを通過しているはずのデータに誤って(あるいはもっと悪いことに、意図的に)依存してしまう 10ということはあり得ません。Relayはあなたのためにヘビーな作業を代行してくれます。

Relayとそのfragmentファーストのアプローチを使うことで、コンポーネントツリー内のデータフローを混乱させることは非常に難しくなります。

もちろん、「queryをデータのソースとして使用する」アプローチの欠点のほとんどは、React.memoshouldComponentUpdateなど、昔ながらの手動での最適化によって多少は緩和されることには注意が必要です。しかし、これはそれ自体がパフォーマンスの問題になる可能性があるだけでなく、ミスを起こしやすいという欠点があります(タスクが面倒であればあるほど、人間は最終的にそれを台無しにしてしまう可能性が高くなります)。一方、Relayは、そんなことを考えなくてもパフォーマンスを維持できるようにしてくれます。

各コンポーネントがキャッシュから独自したデータを受け取ることで、ビューのフルデータが戻ってくるのを待っている間に、すでにストアで利用可能なデータを使ってビューを部分的にレンダリングするなど、Relayの非常にクールで高度な機能を利用することができます。

fragmentのまとめ

ここで少し立ち止まって、Relayがどのようなタイプの作業をしてくれているのかをダイジェストで見てみましょう。

  • 型システムを通じて、Relayは対象のコンポーネントが正確な使い方の正しいGraphQLのデータなしではレンダリングできないことを確認しています。これで失敗が一つ減りました。
  • fragmentを使用する各コンポーネントは、使用するデータが更新された場合にのみ正確に更新されるため、Relayではデフォルトで高いパフォーマンスが得られます。
  • 型を生成することで、Relayはfragmentから得られたデータとの対話が型安全であることを保証しています。ここで注目すべきは、型生成はRelayコンパイラの中核機能であるということです。

Relayのアーキテクチャと哲学では、コンポーネントのデータ依存関係からサーバが提供するデータとその型に至るまで、コンポーネントに関する多くの情報をコンピュータに提供しています。これらすべての情報を利用して、通常であれば開発者である私たちが処理しなければならないようなあらゆる作業を行います。

ビューが複雑になる速度を過小評価するのは簡単です。複雑さとパフォーマンスは、Relayが強制的に従わせる規則によってデフォルトで処理されます。

これにより、開発者には大きな力がもたらされます。

  • ほぼ完全に分離された組み合わせ可能なコンポーネントを構築することができます。
  • コンポーネントのリファクタリングは完全に安全で、Relayは何かを見逃したり、台無しにしたりすることがないようにしてくれます。

再利用可能なコンポーネントを数多く作り始めると、その重要性は過大評価されることはないでしょう。コードベースの大部分で使用されるコンポーネントのリファクタリングが安全であることは、開発者のベロシティにとって非常にクリティカルです。

アプリが成長するにつれて、リファクタリングの容易さと安全性は、高速に開発し続けるために非常に重要になってきます。

Relayの紹介を締めくくる

この記事では多くのことを取り上げました。この記事から何かを得たとするなら、Relayは、保守やリファクタリングが簡単で型安全な、スケーラブルでパフォーマンスが高いアプリケーションを構築することを強制しているということです。

Relayは実際にあなたのために泥臭い仕事を肩代わりしてくれます。そして私たちが示した多くのことは、他のフレームワークを使っても英雄的な努力を通して達成することができますが、私たちはこれらのパターンを強制することがもたらすことができる強力な利点を示したことを願っています。 それらの重要性は誇張ではありません。

注目に値するソフトウェア

Relayは、血と汗と涙、そして何よりも重要な、長い間GraphQLを使って製品を出荷したりメンテナンスしたりしてきた経験と深い洞察力をもとに構築された、注目に値するソフトウェアです。

この記事はかなり長く、かなり濃い内容になっていますが、Relayで何ができるのかについてはまだほんの少ししか触れていません。この記事の最後に、この記事では取り上げていないRelayでできることの詳細をいくつか挙げておきましょう。

  • 楽観的で複雑なキャッシュ更新によるmutation 11
  • subscription
  • SuspenseとConcurrent Modeと完全に統合されており、次世代のReactに対応しています。
  • Relayを使ってローカルな状態を管理することで、Relayをローカルな状態管理にも使えるという一般的なメリットを享受できます(SuspenseとConcurrent Modeとの統合など!)。
  • リストの結果を@stream経由でストリーミングできます
  • サーバレスポンスの読み込みに時間がかかる部分を@deferで遅延させることで、残りの UI をより高速にレンダリングできるようにします。
  • fragmentの再取得とページネーションのためのqueryの自動生成
  • 複雑なキャッシュ管理:キャッシュのサイズを制御し、ビューのデータをキャッシュを使用するかネットワークから取得するかを制御します (またはその両方、または最初にキャッシュを実行してからネットワークを実行します)。
  • 安定した成熟した柔軟なキャッシュ。本当に安定して動きます。 12
  • ユーザーがナビゲーションの開始を示すと、すぐに新しいビューのqueryをプリロードします。queryデータが到着するのを待っている間に、ストアですでに利用可能なデータを使ってビューを部分的にレンダリングします。
  • fragmentの引数(コンポーネントのpropsのようなもの)を定義することで、コンポーネントの組み立てを次のレベルに引き上げることができます。
  • スキーマから得られるデータよりも、グラフ内のデータがどのように接続されているのかをRelayに教えることで、キャッシュからより多くのデータを解決できるようにします(「ある変数を持つトップレベルのフィールドは同じUserを解決できる」と考えてください)。

この記事はここまでですが、Relayのページネーションに関する記事もぜひ読んでみてください。Relayページネーション編では、Relayのパワフルな機能を美しい方法でまとめ、フレームワークにすべての作業を任せることで、どれだけの自動化と驚くべきDXが可能になるのかを紹介しています。詳細はこちらをご覧ください

他にも続けて読める記事をいくつかご紹介します。

お読みいただきありがとうございました!

スペシャルサンクス

この記事の草稿への徹底的なフィードバックをしてくれた Xavier Cazalot, Arnar Þór Sveinsson, Jaap Frolich, Joe Previte, Stepan Parunashvili, Ben Sangster に感謝します!


  1. 訳注:翻訳の許可をくれたGabriel、ありがとう。あんた最高だぜブラザー。

  2. 訳注:2020年8月現在、ReactのSuspenseとConcurrent Modeはexperimental buildでのみ利用可能です。 https://ja.reactjs.org/docs/concurrent-mode-adoption.html hooksなどのRelayのexperimental版にしかないAPIも同様です。

  3. 訳注:2020年5月にrelayで作ったfacebookはリリースされました。

  4. 訳注:GraphQLのoperationとは、QueryまたはMutation、Subscription、Fragmentのことです。

  5. 訳注:全体が1つのグラフになっているので、blogフィールドの中にauthorフィールドがあると考えられます。そうなると、authorフィールドのサブフィールドがfirstNameなどであると考えられます。ここでは要するに、最小単位でまとめられるためにあらゆるコンポーネントのデータを定義できるということです。

  6. 訳注:spreadを展開と訳していますが、ここで言う展開とはスプレッド演算子(…)を使用してfragmentのフィールドをクエリに展開することを言っています。fragmentのデータが展開されるわけではありません。

  7. 訳注:BlogPostHeader.tsのこと。鶏と卵のように聞こえるかもしれませんが、まずBlogPostHeader.tsにqueryを書いてコンパイルすると型定義のファイルが作られるので、それをimportしてレンダリングするコードを書きます。

  8. 訳注:次というのはBlogPostHeader_blogPostの子要素ということ

  9. 訳注:ここで言うツリーダウンとは、親要素から子要素へ、子要素から孫要素へとツリーを降りながらデータを渡すことです。

  10. 訳注:別のコンポーネントが要求して自身が明示的に要求していないデータに依存していると、別のコンポーネントが何らかの理由でそのデータを取得しない場合(もっと悪いとコンポーネントが消えた場合)にデータが欠損します。こうした事象を長期的に手動で完全にコントロールすることは非常に困難です

  11. 訳注:楽観的(optimistic)なキャッシュ更新とは、更新時にサーバー側のレスポンスを待たずにキャッシュを更新することです

  12. 訳注:原文はJust Works (TM) 。知らなかったので本人に聞いたら、Just Works (TM) is a joke. People often refer to software/hardware that’s really stable like “it just works” だそうです。ギャグの解説させてしまってごめん。