本文へスキップ

·読了時間:5分

Relayのアプローチは、最適なランタイムパフォーマンスとアプリケーションの保守性の独自の組み合わせを実現します。この記事では、ほとんどのアプリがデータフェッチで避けられないトレードオフについて説明し、Relayのアプローチによってこれらのトレードオフを回避し、複数のトレードオフ次元で最適な結果を達成する方法について説明します。


ReactなどのコンポーネントベースのUIシステムでは、UIツリーのどこでデータを取得するかという重要な決定があります。データフェッチはUIツリーの任意の場所で実行できますが、トレードオフを理解するために、2つの極端なケースを考えてみましょう。

  • リーフノード:データを使用する各コンポーネント内で直接データを取得する
  • ルートノード:UIのルートですべてのデータを取得し、プロップドリリングを使用してリーフノードに渡す

UIツリーのどこでデータを取得するかは、アプリケーションのパフォーマンスと保守性の複数の次元に関わります。残念ながら、単純なデータフェッチでは、どちらの極端もすべての次元で最適ではありません。これらの次元を見て、データフェッチをリーフに近づけるにつれて何が改善するか、ルートに近づけるにつれて何が改善するかを考えてみましょう。

読み込みエクスペリエンス

  • 🚫 リーフノード:個々のノードがデータを取得する場合、UIは複数のリクエストラウンドトリップを連続して行う必要があり(ウォーターフォール)、UIの各レイヤーが親レイヤーのレンダリングをブロックするため、リクエストカスケードが発生します。さらに、複数のコンポーネントが同じデータを使用する場合、同じデータが複数回フェッチされます。
  • ✅ ルートノード:すべてのデータがルートでフェッチされる場合、単一のリクエストを行い、重複データやカスケードリクエストなしでUI全体をレンダリングします。

サスペンスカスケード

  • 🚫 リーフノード:個々のコンポーネントが個別にデータを取得する必要がある場合、各コンポーネントは最初のレンダリング時にサスペンドします。現在のReactの実装では、サスペンド解除は最寄りの親サスペンス境界からの再レンダリングにつながります。これは、初期ロード中に製品コンポーネントコードをO(n)回評価する必要があることを意味します(nはツリーの深さ)。
  • ✅ ルートノード:すべてのデータがルートでフェッチされる場合、1回だけサスペンドし、製品コンポーネントコードを1回だけ評価します。

コンポーザビリティ

  • ✅ リーフノード:既存のコンポーネントを新しい場所に使用することは、レンダリングするだけなので簡単です。コンポーネントの削除は、レンダリングしないだけなので簡単です。同様に、データ依存関係の追加/削除は完全にローカルで行うことができます。
  • 🚫 ルートノード:既存のコンポーネントを別のコンポーネントの子として追加するには、そのコンポーネントを含むすべてのクエリを更新して新しいデータを取得し、新しいデータをすべての中間レイヤーに渡す必要があります。同様に、コンポーネントを削除するには、これらのデータ依存関係を各ルートコンポーネントまでトレースし、削除したコンポーネントがそのデータの最後のコンシューマーだったかどうかを判断する必要があります。同じダイナミクスは、既存のコンポーネントに新しいデータを追加/削除する場合にも適用されます。

詳細な更新

  • ✅ リーフノード:データが変更されると、そのデータを読み取る各コンポーネントは個別に再レンダリングでき、影響を受けていないコンポーネントの再レンダリングを回避できます。
  • 🚫 ルートノード:すべてのデータはルートから生成されるため、データが更新されると、常にルートコンポーネントが更新され、コンポーネントツリー全体の高価な再レンダリングが強制されます。

Relay

RelayはGraphQLフラグメントとコンパイラビルドステップを利用して、より最適な代替手段を提供します。Relayを使用するアプリでは、各コンポーネントは、必要なデータを宣言するGraphQLフラグメントを定義します。これには、コンポーネントがレンダリングする具体的な値と、レンダリングする各直接子コンポーネントのフラグメント(名前で参照)の両方が含まれます。

ビルド時に、Relayコンパイラはこれらのフラグメントを収集し、アプリケーション内の各ルートノードに対して単一のクエリを構築します。このアプローチが上記の各次元でどのように機能するかを見てみましょう。

  • ✅ 読み込みエクスペリエンス - コンパイラによって生成されたクエリは、必要なすべてのデータを単一のラウンドトリップでフェッチします。
  • ✅ サスペンスカスケード - すべてのデータは単一のリクエストでフェッチされるため、1回だけサスペンドし、ツリーのルートで直接行われます。
  • ✅ コンポーザビリティ - 子コンポーネントのレンダリングに必要なフラグメントデータを含む、コンポーネントからのデータの追加/削除は、単一のコンポーネント内でローカルに行うことができます。コンパイラは、影響を受けるすべてのルートクエリを更新します。
  • ✅ 詳細な更新 - 各コンポーネントはフラグメントを定義しているため、Relayは各コンポーネントによって消費されるデータを正確に認識しています。これにより、Relayは、データ変更時に最小限のコンポーネントセットが再レンダリングされる最適な更新を実行できます。

サマリー

ご覧のとおり、Relayは宣言型のコンポーザブルなデータフェッチ言語(GraphQL)とコンパイラステップを組み合わせることで、上記で概説したすべてのトレードオフ次元で最適な結果を達成できます。

リーフノードルートノードGraphQL/Relay
読み込みエクスペリエンス🚫
サスペンスカスケード🚫
コンポーザビリティ🚫
詳細な更新🚫

·読了時間:4分

Relayチームは、Relay v15のリリースを発表できることを嬉しく思います。このリリースはメジャーバージョンアップであり、いくつかの破壊的変更が含まれていますが、ほとんどのユーザーは影響を受けず、シームレスなアップグレードが可能になると予想されます。v15リリースノートで変更点の完全なリストを確認できます。

Relay 15の新機能

インターフェースでの@refetchableのサポート

以前は、サーバーインターフェースタイプ上のフラグメント定義に@refetchableディレクティブを追加することはできませんでした。

// schema.graphql

interface RefetchableInterfaceFoo @fetchable(field_name: "id") {
id: ID!
}

extend type Query {
fetch__RefetchableInterfaceFoo(id: ID!): RefetchableInterfaceFoo
}

// fragment

fragment RefetchableFragmentFoo on RefetchableInterfaceFoo
@refetchable(queryName: "RefetchableFragmentFooQuery") {
id
}

永続化クエリ改善

URLベースの永続化クエリを使用している場合、クエリを永続化するリクエストに送信するカスタムヘッダーを指定できるようになりました。たとえば、これを使用して、クエリ永続化URLエンドポイントに認証ヘッダーを送信できます。

persistConfig: {
url: 'example.com/persist',
headers: {
Authorization: 'bearer TOKEN'
}
}

ファイルベースの永続化クエリでは、永続化クエリテキストからすべての空白を削除する新しい機能フラグcompact_query_textを追加しました。これにより、ファイルサイズを60%以上削減できます。この新しい機能フラグは、Relay設定ファイルで有効にできます。

persistConfig: {
file: 'path/to/file.json',
algorithm: 'SHA256'
},
featureFlags: {
compact_query_text: true
}

タイプセーフな更新で、不足しているフィールドハンドラーがサポートされるようになりました

タイプセーフなアップデータは、不足しているフィールドハンドラーをサポートするようになりました。以前は、タイプセーフなアップデータでnode(id: 4) { ... on User { name, __typename } }を選択した場合、そのユーザーが別の方法(例:best_friend { name })でフェッチされていると、タイプセーフなアップデータを使用してそのユーザーにアクセスして変更することはできませんでした。

このリリースでは、タイプセーフなアップデータで不足しているフィールドハンドラーのサポートを追加しました。つまり、ノードに対して不足しているフィールドハンドラーが設定されている場合(この例のように)、この不足しているフィールドハンドラーを使用してユーザーの名前を更新できます。

これをサポートするために、不足しているフィールドハンドラーのシグネチャが変更されました。ハンドラーに渡されるrecord引数は、以前はRecordタイプ(データの型なしの詰め合わせ)を受け取っていましたが、現在はReadOnlyRecordProxyを受け取ります。さらに、NormalizationLinkedFieldタイプのフィールド引数は、ReaderLinkedFieldNormalizationLinkedFieldの両方に含まれるプロパティを含むCommonLinkedFieldになりました。

Flowタイプの改善

Flowユーザーは、より多くのRelay APIでgraphqlリテラルから推論された型を取得できるようになりました。usePreloadedQueryuseQueryLoaderuseRefetchableFragmentusePaginationFragmentuseBlockingPaginationFragment APIメソッドの戻り値を明示的に型指定する必要がなくなりました。

Relay Resolverの改善点

前回のリリース以降、開発努力の大部分はRelay Resolvers(グラフ内で派生データ公開するためのメカニズム)の改善に費やされました。Relay Resolversはまだ実験段階であり、APIが将来変更される可能性があることに注意してください。

より簡潔なdocblockタグ

Relay Resolver関数の注釈が簡素化されました。多くの場合、ParentType.field_name: ReturnType構文を使用して、Relay Resolverが公開する新しいフィールドを定義できるようになりました。

変更前

/**
* @RelayResolver
* @onType User
* @fieldName favorite_page
* @rootFragment myRootFragment
*/

変更後

/**
* @RelayResolver User.favorite_page: Page
* @rootFragment myRootFragment
*/

上記の例では、Page型はスキーマ型です。Relay Resolverがスキーマ型を返さない場合は、戻り値の型として固定されたRelayResolverValue値を使用できます。

/**
* @RelayResolver User.best_friend: RelayResolverValue
* @rootFragment myRootFragment
*/

ファイルあたりの複数のResolverの定義

このリリース以前は、ファイルごとに1つのRelay Resolverしか許可されておらず、Relay Resolver関数をデフォルトエクスポートにする必要がありました。Relay 15では、ファイルごとに複数のRelay Resolverを定義し、名前付きエクスポートを使用できるようになりました。

/**
* @RelayResolver User.favorite_page: Page
* @rootFragment favoritePageFragment
*/
function usersFavoritePage(){
...
}

/**
* @RelayResolver User.best_friend: RelayResolverValue
* @rootFragment bestFriendFragment
*/
function usersBestFriend(){
...
}

module.exports = {
usersFavoritePage,
usersBestFriend
}

クエリを楽しんで!

·読了時間:22分
ゲスト投稿

これは、CoinbaseのスタッフエンジニアであるErnie Turnerによるゲスト投稿です。CoinbaseはアプリケーションにRelayを徹底的に採用しており、Relayチームの強力な同盟国です。昨年、彼らはRelay VSCode拡張機能の共同開発に貢献しました。Ernieは、この内部エンジニアリングブログ投稿を共有することに同意しました。

サービス中断中の顧客への最適なエクスペリエンスの提供方法

理想的には、Coinbaseのサービスに障害が発生することはなく、GraphQLスキーマのすべてのフィールドは常に正しく解決されます。これは現実的ではないため、Coinbaseアプリケーションはダウンタイムに耐え、顧客への影響を最小限に抑える必要があります。単一のサービスのダウンタイムが、ユーザーによるアプリ全体の使用または操作を妨げるべきではありません。ただし、アプリケーションが期待どおりに動作していない場合に、ユーザーに問題を伝えることも重要です。リトライボタン付きでダウンタイムを伝えるエラーメッセージを表示することは、ユーザーを欠落したコンテンツや操作できないUIで混乱させるよりも良いエクスペリエンスです。

この投稿では、Relayアプリケーションにおける欠落データの処理に関する一般的なパターンとベストプラクティスについて説明します。

画面アーキテクチャとエラーバウンダリ

GraphQLクエリにおけるサービスのダウンタイムと障害の処理について説明する前に、まず広範な画面アーキテクチャと、React Error Boundariesを正しく使用することでより良いユーザーエクスペリエンスの作成にどのように役立つのかについて説明します。

人生のほとんどのことと同様に、Error Boundariesは適度に使用する必要があります。Coinbase Retailアプリの一般的な画面を見てみましょう。

上記の画面のどのセクションでも、レンダリングに必要なデータの取得に失敗する可能性がありますが、これらの障害への対処方法が、ユーザーがアプリで体験するエクスペリエンスを決定します。たとえば、単一の画面レベルのErrorBoundaryのみを使用すると、エラーの重大度にかかわらず、エラーが発生するとアプリが使用できなくなります。対照的に、各コンポーネントを独自のErrorBoundaryでラップすると、同様に悪いエクスペリエンスにつながる可能性があります。最後に、エラーのあるコンポーネントを完全に省略することは、他の2つのオプションと同じくらい悪いです。万能なアプローチはありません。そこで、それぞれを分解し、なぜそれらが悪いユーザーエクスペリエンスにつながるのかを説明します。

フルスクリーンエラー

上記のUIは、サービスに障害が発生し、この画面のコンポーネントをレンダリングするために必要なデータを取得できなかった場合に表示されるCoinbaseのフルスクリーンエラーフォールバックです。特定の状況では、これは実際に良いユーザーエクスペリエンスを生み出します。ユーザーに何が起こったのかの詳細な情報を提供できない場合がありますが、ほとんどの場合、技術的な原因を提供することは不可能であり、ユーザーのエクスペリエンスを改善するわけでもありません。しかし、何かが正しく動作していないことを伝え、アプリを再び動作させるための明確なリトライボタンを提供しています。

これがユーザーに表示される理由は、資産価格履歴グラフやウォッチリストの状態など、重要なものではないものをロードできないためである場合、画面全体を停止するべきではありません。ビットコインの現在の価格を非表示にし、ビットコインがウォッチリストにあるかどうかを伝えることができないだけで、ユーザーが取引することを妨げるのは、ネガティブなユーザーエクスペリエンスです。

このUIのもう1つの欠点は、ユーザーからすべてのアプリナビゲーションを隠すことです。フルスクリーンエラーを表示する正当な理由がある場合でも、その過程でアプリの残りの部分を隠す必要はありません。ユーザーは依然として別の画面に移動できるはずです。実際には、「フルアプリエラー」ではなく「フルスクリーンエラー」のみを表示する必要があります。

至るところにエラーメッセージ

上記のUIは、多くの点で、さらに悪いです。これは、前のエクスペリエンスの反対側であり、フルスクリーンエラーを表示する方が望ましいでしょう。価格履歴グラフのエラーメッセージは理にかなっていますが、ユーザーがビットコインの価格を見たり、取引ボタンを見つけたりできない場合は、最初のスクリーンショット(ただしナビゲーション付き)を表示する必要があります。この画面の中心的な目標と目的が失われているためです。

この画像は、ErrorBoundariesが過度に普及している様子も示しています。時間範囲セレクターを含む価格履歴グラフ全体には、時間範囲ごとに1つではなく、単一のエラーメッセージのみが必要です。

空のフォールバック

上記のUIは、前の例と同じくらい悪いです。この場合、ErrorBoundariesは空のコンテンツにフォールバックします。特定のUI要素では、これは理にかなっています。ウォッチリストの横にある共有ボタンが欠落していることは、このUIにとって重要ではないため、省略しても問題ありません。しかし、ビットコインの現在の価格、価格履歴グラフ、取引ボタンを非表示にすると、UIが使用できなくなり、やや誤解を招く可能性があります。毎日アプリを使用しないユーザーでも、何かがおかしいことに気付くでしょう。また、ユーザーに障害を再試行するオプションも提供していません。ユーザーは、回復方法のない空のコンテンツのみが表示されます。

代わりにユーザーに何を表示するべきか

次の2つのスクリーンショットは、ユーザーにとってより良いエクスペリエンスの例を示しています。最初のスクリーンショットは、ビットコインの現在の価格を取得できない場合、またはユーザーが取引を許可されているかどうかを判断できない場合にユーザーに表示する必要があるものです。2番目のスクリーンショットは、ビットコインの価格の現在の変化または価格履歴を取得できなかった場合に、ユーザーにとってより良いエクスペリエンスになります。

これらすべてから、画面上のUIセクションを分類する必要性が示唆されます。ユーザーエクスペリエンスにとって重要なもの、ユーザーが表示することを期待するUI、エクスペリエンスにとってオプションのサポートコンテンツなどです。

クリティカルなUI、期待されるUI、オプションのUI

アプリケーション画面のすべてのUI要素が同じであるわけではありません。UIの一部は画面の中心的な情報やインタラクションを定義し、その他はユーザーにとってより多くの情報やヘルプを提供する可能性があります。Coinbaseのアプリケーション設計では、UI要素を「クリティカル」、「期待される」、「オプション」の3つのカテゴリに分類します。

クリティカルなUI要素

画面の重要な情報やユーザーとのインタラクションを定義する部分です。これらの要素がUIにない場合、画面は意味をなさなくなり、これらの要素が欠落している場合、ユーザーは混乱したり、怒ったりする可能性があります。アプリが期待どおりに動作しなかった理由が明確ではないためです。

これらのクリティカルなUI要素を表示するために必要なデータを読み込めなかったとします。その場合、問題を説明する(可能な場合)フルスクリーンエラーメッセージと、欠落しているデータの再要求を簡単に試みることができるリトライボタンをユーザーに表示する必要があります。

クリティカルなUI要素が欠落しているアプリケーションとユーザーがやり取りすると、混乱や怒り、そして何が起こっているのかを完全に知らなくてもユーザーがトランザクションを完了できる場合、資金の損失さえ引き起こす可能性があります。

クリティカルなUI要素の例

  • Coinbaseアプリのホーム画面のユーザーの現在のポートフォリオ残高
  • 注文プレビュー画面の資産価格、支払い方法、購入総額
  • Earn画面のユーザーの生涯収益と資産ごとの収益

期待されるUI要素

期待されるUI要素は、画面の中心的な目的を果たさない可能性がありますが、ほとんどのユーザーが存在することを期待する画面の一部です。期待されるUI要素が画面にない場合、ユーザーは何かが間違っていると思う可能性がありますが、これにより画面の中心的なアクションを実行できなくなるわけではありません。

期待されるUI要素を表示するために必要なデータを読み込めなかった場合、ユーザーに、欠落しているUI要素があることを伝えるコンポーネントローカルのエラーメッセージを表示する必要があります。これらのエラーメッセージには、ユーザーが欠落しているデータの再要求をできるようにする再試行ボタンも付ける必要があります。ローカライズされたエラーは、ユーザーに見過ごされたり、操作されなかったりする可能性が高くなりますが、画面の主要な目的には必須ではないため、ある程度許容できます。

期待されるUI要素が欠落しているアプリケーションとユーザーが対話できるようにすることは許容できるかもしれませんが、何が起こっているのかについて混乱を招く可能性があります。エラーメッセージを伴わずにこれらのUI要素を完全に省略すると、より悪いエクスペリエンスになります。

期待されるUI要素の例

  • 資産購入画面(購入数量を入力する画面)における資産の現在の価格
  • 資産詳細画面の価格履歴グラフ
  • Coinbase Card画面の最近のトランザクションリスト

オプションのUI要素

オプションのUI要素とは、画面の主要な目的を純粋にサポートする画面の一部です。一部のユーザーはこれらの要素の欠落に気付くかもしれませんが、他のユーザーはそれらが存在するはずであること自体に全く気づかないかもしれません。どちらの場合でも、ユーザーは画面上の主要な目標を達成することが妨げられることはありません。

これらのオプションのUI要素を表示するために必要なデータを読み込めなかった場合、UIから完全にそれらを省略する必要があります。ただし、これには次のリスクが伴います。

A. ユーザーは何も欠落していることを知らない可能性がある B. ユーザーがフルスクリーン更新を行わない限り、このUIのデータの再要求方法がない。

開発者はこれらの欠点を考慮し、ネガティブなユーザーエクスペリエンスを引き起こさないようにする必要があります。代わりに、これらのエラーはログに記録されるべきであり、ユーザーエクスペリエンスが理想的でない場合にプロダクトエンジニアに通知されるようにする必要があります。

オプションのUI要素の例

  • 資産詳細画面のオファーカード
  • 取引画面の資産カテゴリセクション(Coinbaseの新着情報、トップムーバーなど)
  • ホーム画面のニュースフィード

上記の画像に戻り、UIのセクションをこれらのカテゴリに分類しましょう。

要素分類の制限

上記の例では、2つの重要なコンポーネント、2つの期待されるコンポーネント、1つのオプションのコンポーネントを持つ画面があります。アプリのほとんどの画面には、少数の重要なUIコンポーネントしか含まれていません。一部の画面では、UI全体が1つの重要なコンポーネントで構成されている場合があります。

期待される要素についても同様です。5つの個別の期待されるUI要素で構成される画面があると、上記のスナップショットのように、「再試行」ボタンがアプリ全体に散らばることになります。1つの画面上の期待される要素と再試行ボタンの数は、可能であれば1つか2つに制限してください。

プルダウン更新

上記のすべてのシナリオにおいて、モバイルアプリのユーザーは、画面上の失敗したリクエストを再試行するためにプルダウン更新を行うことができます。Relayアプリケーションでは、通常、これはフルスクリーンレベルのクエリを再試行することを意味します。画面にデータの欠落によってエラーメッセージまたは非表示コンポーネントがある場合、プルダウン更新を使用すると、常にこれらのエラー状態を修正しようとします。

この分類は主観的なものであり、上記の例はあくまで一つの意見であり、デザイナーやPMは画面の劣化方法について異なる意見を持つ可能性があります。アプリケーションUIを設計する際には、クロスファンクショナルな連携が重要です。チームは、エンジニア、デザイナー、プロダクトマネージャーに相談して、アプリ全体のシームレスでブランドに合った画面を確保する必要があります。

プロダクトマネージャーおよびデザイナーとの連携

Relayがどのように役立つのか

画面をセクションに分類したら、次のステップとして、適切なErrorBoundariesをアプリに追加し、分類に応じてコンポーネントのGraphQLフラグメントを構成します。Relayはここで役立ちます。Relayアプリでの作業経験に基づいて、GraphQLクエリからのデータの欠落に対処する方法に関するいくつかのベストプラクティスを作成しました。

背景

Coinbaseの目標は、Relayチームが推奨するように、null許容スキーマを使用することです。主な理由は、サービスの中断とクエリデータの欠落に対処する方法に関する決定をクライアントエンジニアの手中に置くことです。null許容スキーマがない場合、欠落データの処理方法に関する決定はサーバー側で行われ(null値を最も近いnull許容親にバブルアップすることで)、クライアントコードはこの決定を変更する手段がありません。

この決定は、Relayの@requiredディレクティブの存在によって支えられています。これにより、クライアントエンジニアは、ランタイム時に欠落データの処理方法をRelayに指示するディレクティブを使用して、クエリとフラグメントに注釈を付けることができます。これにより、エンジニアがそうでなければ記述する必要がある定型コードが削減されます。表面上、このディレクティブは非常にシンプルに見えます。非常に単純な3つのオプションしかありません。しかし、さまざまなユースケースでこのディレクティブを使用しようとすると、どのオプションを選択するか、そもそもディレクティブを使用するかどうかという決定が常に明らかではないことがわかります。

@requiredの局所性

@requiredディレクティブの優れた機能の1つは、使用したフラグメントのみに影響を与えることです。同じフィールドをクエリする他のフラグメントの動作を変更することは決してありません。これにより、コンポーネントのスコープ外のものを考慮せずに、ディレクティブを追加または削除できます。これは、同じクエリからデータを取得する場合でも、異なるコンポーネントが異なるカテゴリに分類される可能性があるため重要です。同じクエリのフラグメント内のフィールドに異なる@required引数を付けることができることは、理想的なユーザーエクスペリエンスを構築するために重要です。

action: LOGaction: NONEの使用

LOGNONEのアクションはどちらもランタイム動作は同じですが、LOGは選択したロギングメカニズムにメッセージを送信し、nullとして返されたフィールドへの完全なパスをログに記録します。@requiredディレクティブが必要なほとんどのユースケースでは、NONEよりもLOGを使用する必要があります。NONEが優先されるのは、一部のユーザーに対してフィールドがnullになることが予想される場合のみです。

action: LOGを使用することで作成されたログエントリは、それ自体では操作可能ではない可能性がありますが、将来のエラーのブレッドクラムとして役立つ信号になる可能性があります。エラーの履歴を確認し、特定のフィールドが予期せずnullであったことを確認することで、ユーザーがワークフローで遭遇する可能性のある将来のエラーを追跡するのに役立ちます。

@required(action:LOG/NONE)を使用する場合

LOG/NONEアクションは、コンポーネント内のオプションのUIを表示するために必要なフィールドでのみ使用してください。アプリケーションを設計する際に、これが表示される2つの異なるユースケースがあります。

  1. コンポーネントはオプションのUIであり、フィールドまたはフィールドセットがnullの場合、まったくレンダリングされるべきではありません。
  2. コンポーネントの一部はオプションのUIであり、オブジェクトタイプのフィールドに依存しており、そのオブジェクトはその子フィールドの1つ以上がないと意味がありません。

これらのユースケースの両方を包含するフラグメントを見てみましょう。

fragment MyFragment on Asset {
id
name @required(action: LOG)
slug @required(action: LOG)
color
supply {
total @required(action: LOG)
circulating @required(action: LOG)
}
}

このフラグメントでは、nameフィールドまたはslugフィールドを取得しない場合、フラグメント全体が無効であると述べています。これらのフィールドがサーバーからnullとして返された場合、このコンポーネントをまったくレンダリングできません。このフラグメントは、@required(action: LOG/NONE)ディレクティブを使用してオブジェクトタイプのフィールド全体を無効にする方法も示しています。このフラグメントは、supply.totalフィールドまたはsupply.circulatingフィールドのいずれも存在しない場合、supplyオブジェクト全体自体が無効であり、nullである必要があると述べています。このnull可能性は、コンポーネントのUIのオプションの部分を非表示にするために使用されます。

では、コンポーネントがこのクエリの結果をどのように処理するかを見てみましょう。

const asset = useFragment(
graphql`
fragment MyFragment on Asset {
id
name @required(action: LOG)
slug @required(action: LOG)
color
supply {
total @required(action: LOG)
circulating @required(action: LOG)
}
}
`,
assetRef,
);

// If we couldn't get the required asset name or slug fields, hide this entire UI
if (asset === null) {
return null;
}
// Otherwise hide certain portions of the UI if data is missing
return (
<>
<Title color={asset.color}>{asset.name}</Title>
<Subtitle>{asset.slug}</Subtitle>
{asset.supply && (
<SupplyStats total={asset.supply.total} circulating={asset.supply.circulating} />
)}
</>
);

@requiredディレクティブは、そうでなければ記述する必要がある複雑なnullチェックを削除するため、ここで非常に役立ちます。asset.nameフィールドとasset.slugフィールドの両方がnullかどうかを確認する代わりに、フラグメント全体がnull化されたかどうかを確認し、レンダリングを防止するだけで済みます。SupplyStatsコンポーネントをレンダリングするかどうかを確認する場合も同様です。親フィールドがnullかどうかを確認するだけで、2つのサブフィールドがnullではないことがわかります。

@required(action:THROW)を使用する場合

@required(action: THROW)の使用はより簡単です。このアクションは、期待されるUIコンポーネントまたは重要なUIコンポーネントをレンダリングするために必要なフィールドで使用してください。これらのフィールドがサーバーからnullとして返された場合、コンポーネントは最も近いErrorBoundaryにエラーをスローし、ユーザーにエラーメッセージが表示されます。

ErrorBoundaryがツリーの上部にどれだけあるかは、エラーが発生した場合にUIのどの部分を削除したいかによって異なります。たとえば、資産価格履歴グラフの代わりにユーザーにエラーを表示する場合、時系列ボタンをそのまま表示しておくのは意味がなく、そのUI全体も消える必要があります。しかし、そのために行全体を削除する必要もありません。

ErrorBoundaryには、ユーザーが失敗したクエリを再試行して、後続の試行でデータを取得できるメカニズムを備えるようにしてください。ユーザーが復旧できるように、エラーメッセージと実行可能な要素を常にペアにする必要があります。画面のリロードにプルツーリフレッシュを使用できる(または知っている)とユーザーが仮定すべきではありません。

配列内のフィールドに対する@required(action: THROW)の使用に関する注記

配列フィールドとその配列のフィールドの両方を選択するコンポーネントでTHROWアクションを使用することは、ほとんどの場合避けるべきです。やってはいけない例を示します。

function Component({ assetPriceRef }) {
const { quotes } = useFragment(
graphql`
fragment ComponentFragment on AssetPriceData {
quotes {
# Returns an array of items
timestamp
price @required(action: THROW)
}
}
`,
assetPriceRef,
);
}

このコンポーネントは、quotes配列と、その配列内の各アイテムのtimestampおよびpriceフィールドの両方を選択します。クォートが返ってこなかった場合にユーザーにエラーを表示したい場合は、quotesフィールドにTHROWを置くことは許容されます。しかし、priceフィールドにTHROWを置くことは、その配列内の単一のpriceフィールドがnullの場合でもユーザーにエラーを表示することになります。それはおそらく私たちが望む動作ではありません。過去1日の24個のクォートのうち23個が正しく返ってきた場合、おそらく持っている結果を表示し、空の値を省略するだけで済みます。

代わりに、action: LOG/NONEを使用し、すべてのアイテムではなく、配列内の単一のアイテムだけを無効にするようにします。必要に応じて、配列からnull値をフィルタリングすることもできます。

function Component({ assetPriceRef }) {
const { quotes } = useFragment(
graphql`
fragment ComponentFragment on AssetPriceData {
quotes {
# Returns an array of items
timestamp
price @required(action: LOG)
}
}
`,
assetPriceRef,
);
const validQuotes = quotes.filter(removeNull);
}

フィールドに@requiredを使用しない場合

この質問に対する役に立たない答えは「フィールドが必要ない場合は@requiredを使用しない」です。その答えは、特にフラグメントに12個以上のフィールドがある場合、何が必須で何が必須でないかの決定を単純化しすぎます。しかし、フィールドを必須としてマークするかどうかを決定するために、いくつかのベストプラクティスに従うことができます。繰り返しますが、これらの決定にはPMとデザイナーと協力することが重要です。

@requiredディレクティブを省略する場合と、LOG/NONEアクションで使用する場合の微妙な違いもあります。主な違いは、そのフィールドによってレンダリングされるUIがオプションUIの場合、@requiredディレクティブを省略する必要があることです。

アプリケーションのいくつかのコンポーネントは、異なる分類のUIを組み合わせたものをレンダリングできます。たとえば、単一のコンポーネントが、資産の現在の価格と、ある期間にわたってユーザーの何パーセントが資産を買ったり売ったりしたかの両方の表示を担当する場合があります。つまり、コンポーネントはクリティカルUI(資産価格)とオプションUI(売買統計)の両方を混合しています。

フィールドがオプションのコンテンツのレンダリングに使用され、ユーザーに混乱を与えることなくUIから完全に省略できる場合(オプションUIの定義であることを思い出してください)、そのフィールドに@requiredディレクティブを使用しないでください。代わりに、フィールドがnullの場合にUIを省略するようにコードにチェックを追加してください。

function SomeComponent({ queryRef }) {
const { asset } = useFragment(
graphql`
asset {
latestQuote @required(action: THROW) # Required data
buyPercent # Optional data
}`,
queryRef,
);

return (
<div>
<div>Price: {asset.latestQuote}</div>
{asset.buyPercent !== null && (
<>
<div>Buy Percent: {asset.buyPercent}</div>
<div>Sell Percent: {1 - asset.buyPercent}</div>
</>
)}
</div>
);
}

この例では、buyPercentフィールドに@required(action: LOG/NONE)を使用することは正しくありません。これは、私たちが望む動作ではない、つまりフラグメント全体が無効になるためです。

@requiredディレクティブを省略する場合の別のあまり一般的ではないユースケースは、安全なフォールバック値を提供できる場合です。フィールドにフォールバック/デフォルト値を提供することは、間違った方法で行うと非常に危険です。デフォルト値にフォールバックする可能性のあるケースはいくつかありますが、一般的には非常にまれであり、避けるべきです。ただし、安全なフォールバック値を提供できる場合は、そのフィールドに@requiredを追加する代わりに、フォールバック値を使用する必要があります。

フォールバック値を提供する場合のいくつかのガイドライン

  • 数値フィールド(数値または数値を表す文字列)のフォールバック値は使用しないでください。
    • 欠損値の代わりに0を使用すると、常にユーザーにとってより多くの混乱が生じます。Coinbaseは金融会社であり、ユーザーに正確な値を表示できない場合は、まったく表示すべきではありません。ユーザーの口座残高が$0.00であることを表示するのは、エラーメッセージを表示するよりも明らかに悪いです。これは明らかなユースケースですが、資産の価格変動率、CoinbaseカードのAPY%、またはCoinbase Earnでユーザーが得られる金額などでも、実際の値がない場合は0を表示すべきではありません。
  • ブールフィールドのフォールバック値は注意して使用する必要があります。
    • ブールフィールドのフォールバックの最初の選択肢は、通常、フィールドをfalseに設定することです。ブールフィールドが何を表しているかによっては、falseにフォールバックすると、ユーザーにエラーを表示するよりも悪い顧客体験につながる可能性があります。isEligibleForOfferのようなフィールドに対してfalseにフォールバックするのはおそらく許容されますが、これはおそらくオプションのコンテンツを表示しているためです。hasCoinbaseOneSubscriptionのようなフィールドに対してfalseにフォールバックすることは許容されません。Coinbase Oneの加入者であるユーザーにとって、コンテンツは予想されるものであり、ユーザーはそのUIがアプリに表示されないことに混乱します。
  • 配列フィールドの空の配列へのフォールバックは注意して使用する必要があります。
    • ユーザーにCoinbaseカードのトランザクションリストを表示する場合、空の配列にフォールバックするのは悪いアイデアですが、ユーザーに最近追加された資産のリストを表示する場合は、空の配列にフォールバックしてUIの表示を省略するのはおそらく問題ありません。コンポーネントはすでに空の配列のケースを処理する必要があるからです。
  • 文字列フィールドは通常、nullをそのまま処理する必要があります。
    • 場合によっては、nullとして返される文字列フィールドに対して空の文字列にフォールバックしたい場合がありますが、通常、フィールドをnullのままにする場合と同じコードパスが作成されます。スキーマ内のほとんどの文字列フィールドは空であることが想定されていないため、空の文字列にフォールバックすると、ユーザーが実際のコンテンツの代わりに空の文字列が表示されるというネガティブなユーザーエクスペリエンスが発生する可能性があります。
function SomeComponent({ queryRef }) {
const asset = useFragment(
graphql`
fragment MyFragment on Asset {
canTrade @required(action: THROW) # Required data
hasOfferToStake # Optional data
}
`,
assetRef,
);

const showStakeOffer = asset.hasOfferToStake ?? false;

return (
<div>
{asset.canTrade && <Button>Trade</Button>}
{showStakeOffer && <Button>Stake your currency</Button>}
</div>
);
}

サマリー

このドキュメントから何かを学んだとすれば、それはダウンタイムとサービスの中断を処理する方法について多くの検討が必要であるということです。エラー状態の処理は、世界レベルのアプリケーションを構築する上で重要な部分です。新しい機能のスコープを決定する際には、設計チームとPMチームがあなたのチームと同じページにいることを確認してください。データが不足している場合にユーザーに何を表示するかについてアドバイスが得られない場合は、これらの決定についてチームとして合意に達するように働きかけてください。

Relayは、アプリケーションの障害に対処する上で強力なツールとなる可能性があります。障害に対処する方法を決定する際のRelayのきめ細やかな機能は、これまで慣れていたよりも多くの作業を伴う可能性があります。しかし、この追加の努力は長期的に報われ、アプリケーションでの顧客エクスペリエンスの向上に大きく貢献します。

·12分間の読書

本日、新しいRustベースのRelayコンパイラのプレビューをオープンソースで公開できることを大変嬉しく思います(v13.0.0-rc.1として)!この新しいコンパイラは高速であり、新しいランタイム機能をサポートし、将来のさらなる成長のための強力な基盤を提供します。

このリリースに至るまで、Metaのコードベースは止まることなく成長してきました。私たちの規模では、コードベース内のすべてのクエリをコンパイルするのにかかる時間は、開発者の生産性の直接的な犠牲になって増加していました。JavaScriptベースのコンパイラを最適化するための多くの戦略を試みました(下記参照)が、パフォーマンスの向上を徐々に得る能力は、コードベース内のクエリの数の増加に追いつくことができませんでした。

そこで、Rustでコンパイラを書き直すことにしました。Rustを選んだ理由は、高速で、メモリセーフであり、スレッド間で大きなデータ構造を安全に共有することが容易であるためです。開発は2020年初頭に開始され、その年末に社内でコンパイラがリリースされました。ロールアウトはスムーズで、アプリケーション開発の中断はありませんでした。初期の内部ベンチマークでは、コンパイラのパフォーマンスが平均で約5倍向上し、P95では約7倍向上していることが示されました。それ以降もコンパイラのパフォーマンスをさらに向上させています。

この投稿では、Relayにコンパイラがある理由、新しいコンパイラで実現したいこと、その新しい機能、そしてRust言語を使用することにした理由について説明します。新しいコンパイラの使用を開始するのが急いでいる場合は、コンパイラパッケージのREADMEまたはリリースノートをご覧ください!

Relayにコンパイラがあるのはなぜですか?

Relayには、安定性の保証を提供し、優れたランタイムパフォーマンスを実現するためにコンパイラがあります。

その理由を理解するには、フレームワークの使用ワークフローを検討してください。Relayでは、開発者はGraphQLと呼ばれる宣言型言語を使用して、各コンポーネントに必要なデータ(方法ではなく)を指定します。次に、コンパイラはこれらのコンポーネントのデータ依存関係を、特定のページのすべてのデータを取得するクエリに縫い合わせ、Relayアプリケーションに非常に高いレベルのパフォーマンスと安定性を与えるアーティファクトを事前に計算します。

このワークフローでは、コンパイラは

  • コンポーネントを個別に論理的に推論できるようにし、多くのバグを不可能にし、
  • 可能な限り多くの作業をビルド時にシフトすることで、Relayを使用するアプリケーションのランタイムパフォーマンスを大幅に向上させます。

順番にそれぞれを詳しく調べてみましょう。

ローカル推論のサポート

Relayでは、コンポーネントはGraphQLフラグメントを使用して独自のデータ要件のみを指定します。次に、コンパイラはこれらのコンポーネントのデータ依存関係を、特定のページのすべてのデータを取得するクエリに縫い合わせます。開発者は、データ依存関係がより大きなクエリにどのように適合するかを心配することなく、コンポーネントの記述に集中できます。

ただし、Relayはこのローカル推論をさらに一歩進めます。コンパイラは、Relayランタイムが特定のコンポーネントのフラグメントによって選択されたデータだけを読み取るために使用されるファイルも生成します(これをデータマスキングと呼びます)。そのため、コンポーネントは、明示的に要求しなかったデータにアクセスすることはありません(実際には、タイプレベルだけでなく!)。

したがって、1つのコンポーネントのデータ依存関係を変更しても、別のコンポーネントが表示するデータに影響を与えることはありません。つまり、開発者はコンポーネントを個別に推論できます。これにより、Relayアプリには比類のないレベルの安定性が与えられ、多くのバグを不可能にし、Relayが多くの開発者が同じコードベースに触れることができる理由の重要な部分です。

ランタイムパフォーマンスの向上

Relayは、可能な限り多くの作業をビルド時にシフトするためにコンパイラも使用し、Relayアプリのパフォーマンスを向上させています。

Relayコンパイラは、すべてのコンポーネントのデータ依存関係に関するグローバルな知識を持っているため、手書きの場合と同等、一般的にはそれ以上に優れたクエリを作成できます。これは、実行時に非現実的に遅い最適化方法を用いてクエリを最適化することで実現しています。例えば、生成されたクエリからアクセスできないブランチを削除したり、クエリの同一セクションをフラット化したりします。

そして、これらのクエリはビルド時に生成されるため、RelayアプリケーションはGraphQLフラグメントから抽象構文木(AST)を生成したり、それらのASTを操作したり、実行時にクエリテキストを生成したりすることはありません。代わりに、RelayコンパイラはアプリケーションのGraphQLフラグメントを、ストアへのネットワークデータの書き込み方法と読み出し方法を記述する、事前に計算された最適化された命令(プレーンなJavaScriptデータ構造)に置き換えます。

この構成の追加の利点は、Relayアプリケーションバンドルには、スキーマも、永続化されたクエリを使用する場合にはGraphQLフラグメントの文字列表現も含まれていないことです。これにより、アプリケーションのサイズを削減し、ユーザーの帯域幅を節約し、アプリケーションのパフォーマンスを向上させることができます。

実際、新しいコンパイラはさらに一歩進んで、別の方法でユーザーの帯域幅を節約します。Relayは、ビルド時に各クエリテキストをアプリケーションのサーバーに通知し、一意のクエリIDを生成できるため、アプリケーションは可能性のある非常に長いクエリ文字列をユーザーの低速ネットワーク経由で送信する必要がなくなります。このような永続化されたクエリを使用する場合、ネットワークリクエストを行うためにワイヤ経由で送信する必要があるのは、クエリIDとクエリ変数だけです!

新しいコンパイラによって何が可能になりますか?

コンパイル言語は、動的言語と比較して摩擦を生み出し、開発者の作業を遅くすると認識されることがありますが、Relayはコンパイラを利用して摩擦を軽減し、一般的な開発タスクを容易にします。たとえば、Relayは、ページネーションや新しい変数を使用したクエリの再取得など、微妙に間違えやすい一般的な操作のための高レベルプリミティブを公開します。

これらの操作の共通点は、古いクエリから新しいクエリを生成する必要があり、ボイラープレートと重複が伴うことです。これは自動化の理想的な対象です。Relayはコンパイラのグローバルな知識を利用して、開発者がディレクティブを1つ追加し、関数呼び出しを1つ変更するだけで、ページネーションと再取得を有効にできるようにします。それだけのことです。

しかし、開発者がページネーションを簡単に追加できるようにすることは、ほんの先駆けです。コンパイラに対する私たちのビジョンは、さらに高度なツールを提供して機能の提供とボイラープレートの回避を可能にし、開発者にリアルタイムの支援と洞察を提供し、GraphQLを操作するための他のツールで使用できるパーツで構成されることです。

このプロジェクトの主要な目標は、書き直されたコンパイラのアーキテクチャによって、今後数年間でこのビジョンを実現できるようにすることでした。

そして、まだその段階ではありませんが、各基準において重要な成果を上げています。

たとえば、新しいコンパイラは新しい@requiredディレクティブのサポートを提供します。これは、読み出し時に特定のサブフィールドがnullの場合、親のリンクされたフィールドをnullにするか、エラーをスローします。これは些細な品質向上のように聞こえるかもしれませんが、コンポーネントのコードの半分がnullチェックの場合、@requiredは非常に便利に見えます!

@requiredを使用しないコンポーネント
@requiredを使用した場合

次に、コンパイラは内部専用のVSCode拡張機能を強化します。これは、入力時にフィールド名を自動補完し、ホバー時に型情報を表示するなど、多くの機能を備えています。まだ公開されていませんが、いずれ公開したいと考えています!私たちの経験では、このVSCode拡張機能により、GraphQLデータの操作がはるかに容易で直感的になります。

最後に、新しいコンパイラは、他のGraphQLツールで再利用できる独立したモジュールのシリーズとして記述されました。これはRelayコンパイラプラットフォームと呼びます。内部的には、これらのモジュールは他のコード生成ツールや、異なるプラットフォーム用の他のGraphQLクライアントに再利用されています。

コンパイラのパフォーマンス

これまで、Relayにコンパイラがある理由と、書き直しによって何が実現できるかについて説明しました。しかし、2020年にコンパイラを書き直すことを決定した理由、つまりパフォーマンスについては説明していません。

コンパイラの書き直しを決定する以前は、コードベース内のすべてのクエリをコンパイルするのにかかる時間は、コードベースの成長とともに徐々に、しかし着実に遅くなっていました。パフォーマンスの向上を達成する能力は、コードベース内のクエリの数の増加に追いつくことができず、この窮地からの増分的な解決策は見当たりませんでした。

JavaScriptの限界

以前のコンパイラはJavaScriptで記述されていました。これはいくつかの理由から自然な言語の選択でした。それは、私たちのチームが最も経験豊富な言語であり、Relayランタイムが記述されている言語(コンパイラとランタイムの間でコードを共有できる)、そしてGraphQL参照実装と私たちのモバイルGraphQLツールが記述されている言語でした。

コンパイラのパフォーマンスはしばらくの間妥当なままでした。Node/V8には、高度に最適化されたJITコンパイラとガベージコレクタが付属しており、注意深く使用すれば非常に高速になる可能性があります(私たちはそうしていました)。しかし、コンパイル時間は増加していました。

追いつくためにいくつかの戦略を試みました

  • コンパイラをインクリメンタルにしました。変更に応じて、その変更の影響を受けた依存関係のみを再コンパイルしました。
  • どの変換が遅いか(つまり、flatten)を特定し、できる限りアルゴリズムの改善(メモ化の追加など)を行いました。
  • 公式のgraphql npmパッケージのGraphQLスキーマ表現は、私たちのスキーマを表すために複数のギガバイトのメモリを消費していたため、カスタムフォークに置き換えました。
  • 最もホットなコードパスでプロファイラガイドのマイクロ最適化を行いました。たとえば、オブジェクトの複製と変更に...演算子の使用をやめ、オブジェクトのプロパティをコピー時に明示的にリストアップするようになりました。これにより、オブジェクトの隠れたクラスが保存され、コードがより効率的にJIT最適化されるようになりました。
  • コンパイラを再構築して複数のワーカーにシェルアウトし、各ワーカーが単一のスキーマを処理するようにしました。複数のスキーマを持つプロジェクトはMeta以外では一般的ではないため、これでもほとんどのユーザーはシングルスレッドのコンパイラを使用していました。

これらの最適化は、Relayの急速な内部採用に追いつくには十分ではありませんでした。

最大の課題は、NodeJSが共有メモリを持つマルチスレッドプログラムをサポートしていないことです。できることは、メッセージのやり取りによって通信する複数のワーカーを開始することだけです。

これは、いくつかのシナリオではうまく機能します。たとえば、Jestはこのパターンを採用し、ファイルを変換するテストを実行する際にすべてのコアを利用します。これは、Jestがプロセス間で多くのデータやメモリを共有する必要がないため、適しています。

一方、私たちのスキーマはメモリ内に複数のインスタンスを持つには大きすぎるため、JavaScriptでスキーマごとに1つ以上のスレッドを使用してRelayコンパイラを効率的に並列化する良い方法はありませんでした。

Rustの選択

コンパイラの書き直しを決定した後、多くの言語を評価して、プロジェクトのニーズを満たす言語を検討しました。高速でメモリセーフであり、同時実行をサポートする言語が必要でした。できれば、同時実行のバグは実行時ではなくビルド時に検出されます。同時に、社内で十分にサポートされている言語も必要でした。これにより、選択肢はいくつか絞り込まれました。

  • C++はほとんどの基準を満たしていましたが、学習が難しいと感じました。また、コンパイラは、私たちが望むほど安全性を支援しません。
  • Javaもおそらく適切な選択肢でした。高速でマルチコアですが、低レベルの制御はそれほど提供しません。
  • OCamlはコンパイラ分野で実績のある選択肢ですが、マルチスレッドは困難です。
  • Rustは高速で、メモリセーフで、同時実行をサポートしています。これにより、スレッド間で大きなデータ構造を安全に共有することが容易になります。Rustを取り巻く一般的な興奮、チーム内での以前の経験、Facebookの他のチームによる使用を考慮すると、これは私たちの明確なトップチョイスでした。

社内展開

Rustは素晴らしい選択肢であることがわかりました!主にJavaScript開発者で構成されるチームは、Rustを簡単に採用できました。そして、Rustの高度な型システムはビルド時に多くのエラーを検出し、高い速度を維持するのに役立ちました。

2020年初頭に開発を開始し、年末に社内でコンパイラを展開しました。初期の内部ベンチマークでは、コンパイラのパフォーマンスが平均で約5倍、P95では約7倍向上していることが示されました。それ以来、コンパイラのパフォーマンスをさらに向上させています。

OSSへのリリース

今日、Relay v13の一部として、新しいバージョンのコンパイラを公開できることを嬉しく思います。新しいコンパイラの機能には、次のものがあります。

  • @requiredディレクティブ。
  • @no_inlineディレクティブ。これは、共通のフラグメントのインライン化を防ぎ、生成されるファイルのサイズを小さくするために使用できます。
  • 競合するGraphQLフィールド、引数、ディレクティブの検証
  • TypeScript型生成のサポート
  • リモートクエリ永続化のサポート。

コンパイラに関する詳細情報は、READMEリリースノートをご覧ください!

グラフ上の導出値に開発者がアクセスできるようにしたり、ローカルデータの更新のためのより人間工学的な構文のサポートを追加したり、VSCode拡張機能を完全に充実させたりするなど、コンパイラ内の機能の開発を継続しており、それらをオープンソースでリリースしたいと考えています。今回のリリースを誇りに思っていますが、まだ多くのことが残っています!

謝辞

このブログ投稿に対する素晴らしいフィードバックを提供してくれたJoe Savona、Lauren Tan、Jason Bonta、Jordan Eldredgeに感謝します。コンパイラのバグに関する問題を報告してくれたch1ffa、robrichard、orta、syncに感謝します。TypeScriptサポートを追加してくれたMaartenStaaに感謝します。@requiredディレクティブを有効にするのがいかに難しいかを指摘してくれた@andrewingramに感謝します。これは現在デフォルトで有効になっています。貢献してくれた多くの人々がいます。これは真のコミュニティの努力でした!

·読み時間6分

開発者にとって最も使いやすいRelayのバージョンであるRelay Hooksをリリースし、本日OSSコミュニティで公開できることを大変嬉しく思います! Relay Hooksは、React Hooksを使用してGraphQLデータのフェッチと管理を行うための、再考された新しいAPIセットです。

新しいAPIは、既存のコンテナベースのAPIと完全に互換性があります。新しいコードはRelay Hooksを使用して記述することをお勧めしますが、既存のコンテナを新しいAPIに移行することはオプションであり、コンテナベースのコードは引き続き機能します。

これらのAPIは新しくリリースされましたが、テストされていないわけではありません。Facebook.comの書き換え版は完全にRelay Hooksによって動作しており、これらのAPIは2019年中頃からFacebookでRelayを使用するための推奨方法となっています。

さらに、Relayの最初の開発以降に学んだ、保守可能なデータ駆動型アプリケーションを構築するためのベストプラクティスを凝縮した、書き換えられたガイドツアー更新されたドキュメントもリリースしています。

Relayの使いやすさに関して、まだ改善すべき点がありますが、これらのステップにより、Relayの開発者エクスペリエンスが大幅に向上すると考えています。

リリースされたもの

GraphQLデータを使用するためのReact HooksベースのAPIであるRelay Hooksをリリースしました。また、より安定したバージョンのfetchQueryや、getDataIDを使用してRelayでオブジェクト識別子をカスタマイズする機能(サーバーにグローバルに一意のIDがない場合に役立ちます)など、その他の改善も同時にリリースしました。

リリースされたものの完全なリストについては、リリースノートを参照してください。

Hooks APIの利点

新しくリリースされたAPIは、少なくとも以下の点で開発者エクスペリエンスを向上させます。

  • クエリをフェッチするためのHooksベースのAPI、フラグメントを使用したデータの読み込み、ページネーション、リフェッチ、ミューテーション、サブスクリプションは、同等のコンテナベースのソリューションよりも一般的にコード行数が少なく、間接参照が少なくなります。
  • これらのAPIは、より完全なFlowとTypescriptの網羅性を備えています。
  • これらのAPIはコンパイラの機能を利用して、リフェッチクエリやページネーションクエリの生成など、エラーが発生しやすいタスクを自動化します。
  • これらのAPIには、フェッチポリシーを構成する機能があり、ストアからクエリを満たす条件と、ネットワークリクエストを行う条件を決定できます。
  • これらのAPIを使用すると、コンポーネントがレンダリングされる前にデータのフェッチを開始できます。これは、コンテナベースのソリューションでは実現できません。これにより、ユーザーにデータを表示するまでの時間を短縮できます。

以下の例は、新しいAPIの利点の一部を示しています。

異なる変数を使用したフラグメントのリフェッチ

まず、Hooks APIを使用して異なる変数を持つフラグメントをリフェッチする方法を見てみましょう。

type Props = {
comment: CommentBody_comment$key,
};

function CommentBody(props: Props) {
const [data, refetch] = useRefetchableFragment<CommentBodyRefetchQuery, _>(
graphql`
fragment CommentBody_comment on Comment
@refetchable(queryName: "CommentBodyRefetchQuery") {
body(lang: $lang) {
text
}
}
`,
props.comment,
);

return <>
<CommentText text={data?.text} />
<Button
onClick={() =>
refetch({ lang: 'SPANISH' }, { fetchPolicy: 'store-or-network' })
}>
>
Translate
</Button>
</>
}

これを同等のコンテナベースの例と比較してください。Hooksベースの例では、行数が少なく、開発者が手動でリフェッチクエリを作成する必要がなく、リフェッチ変数の型チェックが行われ、ストア内のデータからクエリを満たすことができる場合はネットワークリクエストが発行されないことが明示的に示されています。

コンポーネントのレンダリング前にデータのフェッチを開始する

新しいAPIを使用すると、コンポーネントがレンダリングされる前にデータのフェッチを開始することで、ユーザーにコンテンツをより迅速に表示できます。この方法でデータをプリフェッチすることは、コンテナベースのAPIでは不可能です。次の例を考えてみてください。

const UserQuery = graphql`
query UserLinkQuery($userId: ID!) {
user(id: $userId) {
user_details_blurb
}
}
`;

function UserLink({ userId, userName }) {
const [queryReference, loadQuery] = useQueryLoader(UserQuery);

const [isPopoverVisible, setIsPopoverVisible] = useState(false);

const maybePrefetchUserData = useCallback(() => {
if (!queryReference) {
// calling loadQuery will cause this component to re-render.
// During that re-render, queryReference will be defined.
loadQuery({ userId });
}
}, [queryReference, loadQuery]);

const showPopover = useCallback(() => {
maybePrefetchUserData();
setIsPopoverVisible(true);
}, [maybePrefetchUserData, setIsPopoverVisible]);

return <>
<Button
onMouseOver={maybePrefetchUserData}
onPress={showPopover}
>
{userName}
</Button>
{isPopoverVisible && queryReference && (
<Popover>
<React.Suspense fallback={<Glimmer />}>
<UserPopoverContent queryRef={queryReference} />
</React.Suspense>
</Popover>
)}
</>
}

function UserPopoverContent({queryRef}) {
// The following call will Suspend if the request for the data is still
// in flight:
const data = usePreloadedQuery(UserQuery, queryRef);
// ...
}

この例では、クエリをローカルキャッシュ内のデータから満たすことができない場合、ユーザーがボタンにカーソルを合わせるとネットワークリクエストが開始されます。したがって、ボタンが最終的に押されたとき、ユーザーはコンテンツをより早く見ることができます。

これに対し、コンテナベースのAPIは、コンポーネントがレンダリングされるときにネットワークリクエストを開始します。

データフェッチのためのHooksとSuspense

両方の例でSuspenseを使用していることに気付かれたかもしれません。

Relay Hooksは一部のAPIでSuspenseを使用していますが、ReactでのデータフェッチのためのSuspenseの使用に関するサポート、一般的なガイダンス、および要件はまだ準備ができておらず、Reactチームはまだ今後のリリースでこのガイダンスがどうなるかを定義しています。SuspenseをReact 17で使用する場合には、いくつかの制限があります。

それにもかかわらず、これらのAPIがReactの今後のリリースをサポートする正しい軌道に乗っていると確信しているため、Relay Hooksをリリースしました。RelayのSuspense実装の一部はまだ変更される可能性がありますが、Relay Hooks API自体は安定しています。内部的に広く採用されており、1年以上本番環境で使用されています。

このトピックの詳細については、Suspenseの互換性Suspenseを使用した読み込み状態を参照してください。

今後の手順

入門ガイド移行ガイド、およびガイドツアーをご覧ください。

謝辞

Relay Hooksのリリースは、React Dataチームだけの仕事ではありませんでした。実現に協力してくれた貢献者の方々に感謝申し上げます。

@0xflotus, @AbdouMoumen, @ahmadrasyidsalim, @alexdunne, @alloy, @andrehsu, @andrewkfiedler, @anikethsaha, @babangsund, @bart88, @bbenoist, @bigfootjon, @bondz, @BorisTB, @captbaritone, @cgriego, @chaytanyasinha, @ckknight, @clucasalcantara, @damassi, @Daniel15, @daniloab, @earvinLi, @EgorShum, @eliperkins, @enisdenjo, @etcinit, @fabriziocucci, @HeroicHitesh, @jaburx, @jamesgeorge007, @janicduplessis, @jaroslav-kubicek, @jaycenhorton, @jaylattice, @JonathanUsername, @jopara94, @jquense, @juffalow, @kafinsalim, @kyarik, @larsonjj, @leoasis, @leonardodino, @levibuzolic, @liamross, @lilianammmatos, @luansantosti, @MaartenStaa, @MahdiAbdi, @MajorBreakfast, @maraisr, @mariusschulz, @martinbooth, @merrywhether, @milosa, @mjm, @morrys, @morwalz, @mrtnzlml, @n1ru4l, @Nilomiranda, @omerzach, @orta, @pauloedurezende, @RDIL, @RicCu, @robrichard, @rsmelo92, @SeshanPillay25, @sibelius, @SiddharthSham, @stefanprobst, @sugarshin, @taion, @thedanielforum, @theill, @thicodes, @tmus, @TrySound, @VinceOPS, @visshaljagtap, @Vrq, @w01fgang, @wincent, @wongmjane, @wyattanderson, @xamgore, @yangshun, @ymittal, @zeyap, @zpao and @zth.

オープンソースプロジェクトrelay-hooksにより、コミュニティはRelayとReact Hooksを試すことができ、貴重なフィードバックの源となりました。useSubscriptionフックのアイデアは、そのリポジトリのissueに由来しています。このプロジェクトを推進し、私たちのオープンソースコミュニティで重要な役割を果たしてくれた@morrysに感謝します。

実現に協力していただきありがとうございます!