paint-brush
GraphQL の力による tRPC のシンプルさ@wunderstef
6,554 測定値
6,554 測定値

GraphQL の力による tRPC のシンプルさ

Stefan Avram 13m2022/12/14
Read on Terminal Reader
Read this story w/o Javascript

長すぎる; 読むには

featured image - GraphQL の力による tRPC のシンプルさ
Stefan Avram  HackerNoon profile picture
0-item


私は tRPC の大ファンです。サーバーから型をエクスポートし、それらをクライアントにインポートして、コンパイル時の手順がなくても、両者の間で型安全な契約を結ぶというアイデアは、単純に素晴らしいものです。 tRPC に関わっているすべての人に、あなたは素晴らしい仕事をしています。


とはいえ、tRPC と GraphQL の比較を見ていると、リンゴとオレンジを比較しているように思えます。


これは、GraphQL と tRPC に関する公の議論を見ると特に明白になります。たとえば、 theoによるこの図を見てください。

Theoはこの図を詳細に説明しましたが、一見したところ、非常に理にかなっています。 tRPC はコンパイル時のステップを必要とせず、開発者の経験は素晴らしく、GraphQL よりもはるかに単純です。


しかし、それは本当に全体像なのでしょうか? それとも、このシンプルさは他の何かを犠牲にして達成されているのでしょうか? tRPC と GraphQL の両方を使用して簡単なアプリを作成して調べてみましょう。

tRPC で Facebook のクローンを作成しましょう

ニュース フィード用のページ、フィード リスト用のコンポーネント、およびフィード アイテム用のコンポーネントを含むファイル ツリーを想像してみましょう。


 src/pages/news-feed ├── NewsFeed.tsx ├── NewsFeedList.tsx └── NewsFeedItem.tsx


フィード ページの最上部には、ユーザー、通知、未読メッセージなどに関する情報が必要です。


フィード リストをレンダリングするときは、フィード アイテムの数、別のページがあるかどうか、およびそれを取得する方法を知る必要があります。


フィード アイテムについては、作成者、コンテンツ、いいねの数、およびユーザーがそれを気に入ったかどうかを知る必要があります。


tRPC を使用する場合は、このすべてのデータを一度にロードする「手順」を作成します。ページの上部でこのプロシージャを呼び出し、データをコンポーネントに伝播します。


フィード コンポーネントは次のようになります。

 import { trpc } from '../utils/trpc' export function NewsFeed() { const feed = trpc.newsFeed.useQuery() return ( <div> <Avatar>{feed.user}</Avatar> <UnreadMessages> {feed.unreadMessages} unread messages </UnreadMessages> <Notifications> {feed.notifications} notifications </Notifications> <NewsFeedList feed={feed} /> </div> ) }


次に、フィード リスト コンポーネントを見てみましょう。

 export function NewsFeedList({ feed }) { return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


最後に、フィード項目コンポーネント:

 export function NewsFeedItem({ item }) { return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


私たちはまだ単一のチームであり、すべてが TypeScript であり、単一のコードベースであり、tRPC を使用していることを覚えておいてください。


ページをレンダリングするために実際に必要なデータを考えてみましょう。ユーザー、未読メッセージ、通知、フィード アイテム、フィード アイテムの数、次のページ、作成者、コンテンツ、いいねの数、およびユーザーがそれを気に入ったかどうかが必要です。


これらすべてに関する詳細情報はどこにありますか?アバターのデータ要件を理解するには、 Avatarコンポーネントを確認する必要があります。未読メッセージと通知のコンポーネントがあるため、それらも確認する必要があります。フィード リスト コンポーネントには、アイテム数、次のページ、およびフィード アイテムが必要です。フィード項目コンポーネントには、各リスト項目の要件が含まれています。


このページのデータ要件を理解したい場合は、合計で 6 つの異なるコンポーネントを確認する必要があります。同時に、各コンポーネントに実際にどのようなデータが必要かはわかりません。 tRPC にはそのような概念がないため、各コンポーネントが必要なデータを宣言する方法はありません。


これは 1 つのページにすぎないことに注意してください。似ているがわずかに異なるページを追加するとどうなりますか?


ニュース フィードのバリエーションを作成しているとしましょう。ただし、最新の投稿を表示する代わりに、最も人気のある投稿を表示しています。


いくつかの変更を加えるだけで、ほぼ同じコンポーネントを使用できます。人気のある投稿には、追加のデータを必要とする特別なバッジがあるとしましょう。


このための新しい手順を作成する必要がありますか?あるいは、既存の手順にさらにいくつかのフィールドを追加することもできますか?


ページをどんどん追加する場合、このアプローチは適切に拡張されますか?これは、REST API で発生した問題のように聞こえませんか?これらの問題には、オーバーフェッチングやアンダーフェッチングなどの有名な名前さえありますが、N+1 問題について話し合うまでには至っていません。


ある時点で、プロシージャを 1 つのルート プロシージャと複数のサブプロシージャに分割することを決定する場合があります。ルート レベルで配列をフェッチし、配列内の各項目に対して、別のプロシージャを呼び出してさらにデータをフェッチする必要がある場合はどうなるでしょうか。


もう 1 つの方法として、 trpc.newsFeed.useQuery({withPopularBadges: true})など、初期バージョンのプロシージャに引数を導入する方法があります。


これは機能しますが、GraphQL の機能を再発明し始めているように感じます。

GraphQL で Facebook のクローンを作成してみましょう

では、これを GraphQL と対比してみましょう。 GraphQL には Fragments の概念があり、各コンポーネントのデータ要件を宣言できます。 Relay などのクライアントを使用すると、ページの上部で単一の GraphQL クエリを宣言し、子コンポーネントからのフラグメントをクエリに含めることができます。


このように、まだページの上部で 1 つのフェッチを行っていますが、実際にはフレームワークが各コンポーネントのデータ要件の宣言と収集をサポートしています。

GraphQL、Fragments、および Relay を使用した同じ例を見てみましょう。 Copilot を使用してコードを記述しているため、怠惰な理由から、コードは 100% 正しいわけではありませんが、実際のアプリでの表示に非常に近いはずです。


 import { graphql } from 'react-relay' export function NewsFeed() { const feed = useQuery(graphql` query NewsFeedQuery { user { ...Avatar_user } unreadMessages { ...UnreadMessages_unreadMessages } notifications { ...Notifications_notifications } ...NewsFeedList_feed } `) return ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


次に、フィード リスト コンポーネントを見てみましょう。フィード リスト コンポーネントは、それ自体のフラグメントを宣言し、フィード アイテム コンポーネントのフラグメントを含めます。


 import { graphql } from 'react-relay' export function NewsFeedList({ feed }) { const list = useFragment( graphql` fragment NewsFeedList_feed on NewsFeed { items { ...NewsFeedItem_item } hasNextPage } `, feed ) return ( <div> <h1>News Feed</h1> <p>There are {feed.items.length} items</p> {feed.items.map((item) => ( <NewsFeedItem item={item} /> ))} {feed.hasNextPage && ( <button onClick={feed.fetchNextPage}>Load more</button> )} </div> ) }


最後に、フィード項目コンポーネント:

 import { graphql } from 'react-relay' export function NewsFeedItem({ item }) { const item = useFragment( graphql` fragment NewsFeedItem_item on NewsFeedItem { author { name } content likes hasLiked } `, item ) return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> </div> ) }


次に、フィード アイテムに人気のあるバッジを使用して、ニュース フィードのバリエーションを作成しましょう。 @includeディレクティブを使用して、人気のあるバッジ フラグメントを条件付きで含めることができるため、同じコンポーネントを再利用できます。

 import { graphql } from 'react-relay' export function PopularNewsFeed() { const feed = useQuery(graphql` query PopularNewsFeedQuery($withPopularBadges: Boolean!) { user { ...Avatar_user } unreadMessages { ...UnreadMessages_unreadMessages } notifications { ...Notifications_notifications } ...NewsFeedList_feed } `) return ( <div> <Avatar user={feed.user} /> <UnreadMessages unreadMessages={feed.unreadMessages} /> <Notifications notifications={feed.notifications} /> <NewsFeedList feed={feed} /> </div> ) }


次に、更新されたフィード リスト アイテムがどのようになるかを見てみましょう。

 import { graphql } from 'react-relay' export function NewsFeedItem({ item }) { const item = useFragment( graphql` fragment NewsFeedItem_item on NewsFeedItem { author { name } content likes hasLiked ...PopularBadge_item @include(if: $withPopularBadges) } `, item ) return ( <div> <h2>{item.author.name}</h2> <p>{item.content}</p> <button onClick={item.like}>Like</button> {item.popularBadge && <PopularBadge badge={item.popularBadge} />} </div> ) }


ご覧のとおり、GraphQL は非常に柔軟で、同じページのバリエーションを含む複雑な Web アプリケーションを、コードをあまり複製することなく構築できます。

GraphQL Fragments を使用すると、コンポーネント レベルでデータ要件を宣言できます

さらに、GraphQL Fragments を使用すると、各コンポーネントのデータ要件を明示的に宣言できます。これにより、ページの上部に持ち上げられ、単一のリクエストでフェッチされます。


GraphQL は API 実装をデータ取得から分離します

tRPC の優れた開発者エクスペリエンスは、API 実装とデータ消費という 2 つの非常に異なる懸念を 1 つの概念に統合することによって実現されます。


これはトレードオフであることを理解することが重要です。フリーランチはありません。 tRPC の単純さは柔軟性を犠牲にしています。


GraphQL では、スキーマ設計により多くの投資を行う必要がありますが、この投資は、アプリケーションを多くの関連ページにスケーリングする必要がある瞬間に報われます。


API 実装をデータ フェッチから分離することで、同じ API 実装をさまざまなユース ケースに再利用することがはるかに簡単になります。

API の目的は、内部実装を外部インターフェースから分離することです。

API を構築する際に考慮すべき重要な側面がもう 1 つあります。独自のフロントエンド専用の内部 API から開始する場合、tRPC がこのユース ケースに最適な場合があります。


しかし、あなたの努力の将来はどうですか?チームを成長させる可能性はどのくらいですか?他のチームやサード パーティが API を使用する可能性はありますか?

REST と GraphQL はどちらも、コラボレーションを念頭に置いて構築されています。すべてのチームが TypeScript を使用しているわけではありません。会社の境界を越えている場合は、理解しやすく消費しやすい方法で API を公開する必要があります。


REST および GraphQL API を公開および文書化するためのツールは多数ありますが、tRPC は明らかにこのユース ケース用に設計されていません。


したがって、tRPC から始めるのは素晴らしいことですが、ある時点でそれを超えて成長する可能性が非常に高くなります。


tRPC API から OpenAPI 仕様を生成することは確かに可能であり、ツールは存在しますが、最終的に API をサード パーティに公開することに依存するビジネスを構築している場合、RPC は適切に設計された REST と競合することはできません。 GraphQL API。

結論

最初に述べたように、私は tRPC の背後にあるアイデアの大ファンです。これは正しい方向への大きな一歩であり、データの取得がより簡単になり、開発者にとってより使いやすくなります。


一方、GraphQL、Fragments、および Relay は、複雑な Web アプリケーションの構築に役立つ強力なツールです。同時に、セットアップは非常に複雑で、コツをつかむまでには多くの概念を学ぶ必要があります。


tRPC はすぐに使い始めることができますが、ある時点でそのアーキテクチャに対応できなくなる可能性が非常に高くなります。今日、GraphQL または tRPC のいずれかに賭ける決定を下している場合は、プロジェクトがどこに向かうのかを考慮する必要があります。将来。データ取得要件はどのくらい複雑になりますか?複数のチームが API を使用しますか? API をサードパーティに公開する予定はありますか?


見通し

とはいえ、両方の長所を組み合わせることができたらどうでしょうか? tRPC のシンプルさと GraphQL のパワーを組み合わせた API クライアントはどのようになるでしょうか? Fragments と Relay の力を tRPC のシンプルさと組み合わせて提供する、純粋な TypeScript API クライアントを構築できますか?


tRPC のアイデアを取り入れて、GraphQL と Relay から学んだことと組み合わせると想像してみてください。


ここに少しプレビューがあります:


 // src/pages/index.tsx import { useQuery } from '../../.wundergraph/generated/client' import { Avatar_user } from '../components/Avatar' import { UnreadMessages_unreadMessages } from '../components/UnreadMessages' import { Notifications_notifications } from '../components/Notifications' import { NewsFeedList_feed } from '../components/NewsFeedList' export function NewsFeed() { const feed = useQuery({ operationName: 'NewsFeed', query: (q) => ({ user: q.user({ ...Avatar_user.fragment, }), unreadMessages: q.unreadMessages({ ...UnreadMessages_unreadMessages.fragment, }), notifications: q.notifications({ ...Notifications_notifications.fragment, }), ...NewsFeedList_feed.fragment, }), }) return ( <div> <Avatar /> <UnreadMessages /> <Notifications /> <NewsFeedList /> </div> ) } // src/components/Avatar.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' export const Avatar_user = Fragment({ on: 'User', fragment: ({ name, avatar }) => ({ name, avatar, }), }) export function Avatar() { const data = useFragment(Avatar_user) return ( <div> <h1>{data.name}</h1> <img src={data.avatar} /> </div> ) } // src/components/NewsFeedList.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' import { NewsFeedItem_item } from './NewsFeedItem' export const NewsFeedList_feed = Fragment({ on: 'NewsFeed', fragment: ({ items }) => ({ items: items({ ...NewsFeedItem_item.fragment, }), }), }) export function NewsFeedList() { const data = useFragment(NewsFeedList_feed) return ( <div> {data.items.map((item) => ( <NewsFeedItem item={item} /> ))} </div> ) } // src/components/NewsFeedItem.tsx import { useFragment, Fragment } from '../../.wundergraph/generated/client' export const NewsFeedItem_item = Fragment({ on: 'NewsFeedItem', fragment: ({ id, author, content }) => ({ id, author, content, }), }) export function NewsFeedItem() { const data = useFragment(NewsFeedItem_item) return ( <div> <h1>{data.title}</h1> <p>{data.content}</p> </div> ) }


どう思いますか?このようなものを使用しますか?コンポーネント レベルでデータの依存関係を定義することに価値があると思いますか?それとも、ページ レベルでリモート プロシージャを定義することに固執したいと思いますか?あなたの考えを聞きたいです...


現在、React、NextJS、およびその他すべてのフレームワークに最適なデータ フェッチ エクスペリエンスを構築するための設計段階にあります。このトピックに興味がある場合は、 Twitter で私をフォローして最新情報を入手してください。


ディスカッションに参加して RFC について話し合う場合は、お気軽にDiscordサーバーに参加してください。