メインコンテンツへスキップ

レジリエントなRelayアプリケーション

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

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

サービス中断時の顧客への最適な体験の提供方法

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

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

画面アーキテクチャとエラー境界

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

人生の多くのことと同様に、エラー境界は適度に使用する必要があります。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カード画面の最近のトランザクション一覧

オプションのUI要素

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

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

A. ユーザーは何も欠けていることに気付かない可能性がある B. ユーザーが画面全体を更新しない限り、このUIのデータの再要求方法がありません。

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

オプションのUI要素の例

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

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

要素分類の制限

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

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

プルダウン更新

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

この分類は主観的であり、上記の例は単なる1つの意見であり、デザイナーまたは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フィールドが1つでも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のきめ細かい機能は、これまで慣れていたよりも多くの作業を伴う可能性があります。しかし、この追加の努力は最終的に報われ、アプリケーションでの顧客体験の向上に大きく貢献します。