2022年4月27日

Remix、Prisma & MongoDBでフルスタックアプリを構築する:CRUD、フィルタリング、ソート

読了時間17分

MongoDB、Prisma、Remixを使ってフルスタックアプリケーションをゼロから構築する方法を学ぶシリーズの第3回へようこそ! 今回は、ユーザーの称賛フィードを表示し、他のユーザーに称賛を送ることができるアプリケーションの主要部分を構築します。

Build A Fullstack App with Remix, Prisma & MongoDB: CRUD, Filtering & Sorting

目次

はじめに

このシリーズの前回では、アプリケーションのサインインフォームとサインアップフォームを構築し、セッションベースの認証を実装しました。 また、ユーザーのプロファイルデータを保持するUserモデルに新しい埋め込みドキュメントを組み込むために、Prismaスキーマを更新しました。

今回は、アプリケーションの主要な機能である称賛フィードを構築します。 各ユーザーは、他のユーザーから送られた称賛のフィードを持ちます。 ユーザーは他のユーザーに称賛を送ることもできます。

さらに、フィード内の称賛を見つけやすくするために、検索とフィルタリングを実装します。

このプロジェクトの開始点は、GitHubリポジトリのpart-2ブランチで入手できます。 今回の最終結果を確認したい場合は、part-3ブランチに進んでください。

開発環境

提供されている例に従うには、以下が必要です...

  • ... Node.js がインストールされていること。
  • ... Git がインストールされていること。
  • ... TailwindCSS VSCode Extension がインストールされていること。(オプション)
  • ... Prisma VSCode Extension がインストールされていること。(オプション)

注記:オプションの拡張機能は、TailwindとPrismaに非常に優れたインテリセンスと構文ハイライトを追加します。

ホームルートを構築する

アプリケーションの主要なセクションは、/homeルートに配置されます。app/routesフォルダーにhome.tsxファイルを追加して、そのルートを設定します。

この新しいファイルは、今のところHomeという関数コンポーネントと、ログインしていないユーザーをログイン画面にリダイレクトするloader関数をエクスポートする必要があります。

この/homeルートは、ベースURLではなく、アプリケーションのメインページとして機能します。

現在、app/routes/index.tsxファイル(/ルート)はReactコンポーネントをレンダリングします。 そのルートは、ユーザーを/homeまたは/loginルートのいずれかにリダイレクトするだけである必要があります。 その機能を達成するために、代わりにリソースルートを設定します。

リソースルート

リソースルートは、コンポーネントをレンダリングせず、代わりに任意タイプのレスポンスを返すことができるルートです。 シンプルなAPIエンドポイントと考えてください。 /ルートの場合、302ステータスコードでredirectレスポンスを返すようにします。

既存のapp/routes/index.tsxファイルを削除し、代わりにリソースルートを定義するindex.tsファイルに置き換えます。

注記:このルートはコンポーネントをレンダリングしないため、ファイルの拡張子が.tsに変更されました。

上記のloaderは、ユーザーが/ルートにアクセスしたときに、最初にユーザーがログインしているかどうかを確認します。 有効なセッションがない場合、requireUserId関数は/loginにリダイレクトします。

有効なセッションがある場合、loader/homeページへのredirectを返します。

ユーザーリストパネルを追加する

ホームページの開始として、サイトのユーザーを画面の左側にリスト表示するコンポーネントを構築します。

app/componentsフォルダーにuser-panel.tsxという名前の新しいファイルを作成します。

これにより、ユーザーのリストを含むサイドパネルが作成されます。 ただし、コンポーネントは静的であり、アクションを実行したり、何らかの方法で変化したりすることはありません。

ユーザーのリストを追加してこのコンポーネントをより動的にする前に、app/routes/home.tsxページにインポートして、ページにレンダリングします。

上記のコードは、新しいコンポーネントとLayoutコンポーネントをインポートし、レイアウト内に新しいコンポーネントをレンダリングします。

すべてのユーザーをクエリし、結果をソートする

パネル内にユーザーのリストを実際に表示する必要があります。 ユーザー関連の関数が配置されるファイルが既に存在する必要があります:app/utils/user.server.ts

データベース内のすべてのユーザーをクエリする新しい関数をそのファイルに追加します。 この関数は、userIdパラメーターを受け取り、ユーザーのファーストネームで昇順に結果をソートする必要があります。

whereフィルターは、iduserIdパラメーターと一致するドキュメントを除外します。 これは、現在ログインしているユーザーを除くすべてのuserを取得するために使用されます。

注記:埋め込みドキュメント内のフィールドでソートするのがいかに簡単であるかに注目してください。

app/routes/home.tsxで、その新しい関数をインポートし、loader内で呼び出します。 次に、Remixのjsonヘルパーを使用してユーザーリストを返します。

注記loader関数内で実行されるコードは、クライアント側のコードには公開されません。 この素晴らしい機能についてRemixに感謝できます!

データベースにユーザーがいて、loader内でusers変数をoutputした場合、自分を除くすべてのユーザーのリストが表示されるはずです。

注記profile埋め込みドキュメント全体は、明示的に含める必要なく、ネストされたオブジェクトとして取得されました。

これでデータが利用可能になります。 それを使って何かをする時が来ました!

ユーザーをユーザーパネルに提供する

UserPanelコンポーネントに新しいusersプロパティを設定します。

ここで使用されているUser型はPrismaによって生成され、Prisma Clientを介して利用可能です。 Remixは、フルスタックフレームワークでエンドツーエンドのタイプ安全性​​を非常に簡単に実現できるため、Prismaとうまく連携します。

注記:エンドツーエンドのタイプ安全性​​は、データ​​の形状が変化するにつれて、スタック全体のタイプが同期された状態に保たれるときに発生します。

app/routes/home.tsxで、UserPanelコンポーネントにユーザーを提供できるようになりました。 useLoaderDataフックをRemixからインポートします。これにより、loader関数から返された任意のデータにアクセスし、それを使用してusersデータにアクセスできます。

コンポーネントは、操作するusersを持つようになります。 これで、それらを表示する必要があります。

ユーザー表示コンポーネントを構築する

リスト項目は、今のところユーザーのファーストネームとラストネームの最初の文字を持つ円として表示されます。

app/componentsuser-circle.tsxという名前の新しいファイルを作成し、次のコンポーネントを追加します。

このコンポーネントは、userドキュメントからprofileデータのみを渡すため、Prismaによって生成されたProfile型を使用します。

また、クリックアクションを提供したり、スタイルをカスタマイズするための追加クラスを追加したりできる、いくつかの構成可能なオプションがあります。

app/components/user-panel.tsxで、この新しいコンポーネントをインポートし、<p>Users go here</p>をレンダリングする代わりに、各ユーザーに1つずつレンダリングします。

素晴らしい! これで、ユーザーはホームページの左側の素敵な列にレンダリングされます。 この時点でのサイドパネルの唯一の非機能的な部分は、サインアウトボタンです。

ログアウト機能を追加する

app/routeslogout.tsという名前の別のリソースルートを追加します。これは、呼び出されたときにログアウトアクションを実行します。

このルートは、POSTとGETの2つの可能なアクションを処理します。

  • POST:これは、このシリーズの前回で記述されたlogout関数をトリガーします。
  • GETGETリクエストが行われた場合、ユーザーはホームページに送信されます。

app/components/user-panel.tsのサインアウトボタンの周りに、送信時にこのルートにPOSTするformを追加します。

これで、ユーザーはアプリケーションからサインアウトできるようになりました! POSTリクエストに関連付けられたセッションを持つユーザーはサインアウトされ、そのセッションは破棄されます。

称賛を送る機能を追加する

ユーザーリストのユーザーがクリックされると、フォームを提供するモーダルがポップアップ表示されるはずです。 このフォームを送信すると、称賛がデータベースに保存されます。

このフォームには、次の機能があります。

  • 誰に称賛を送っているかの表示。
  • ユーザーへのメッセージを記入できるテキストエリア。
  • 投稿の背景色とテキスト色を選択できるスタイリングオプション。
  • 投稿に絵文字を追加できる絵文字セレクター。
  • 投稿がどのように見えるかの正確なプレビュー。

Prismaスキーマを更新する

スキーマでまだ定義されていない、保存および表示するデータポイントがいくつかあります。 変更する必要があるもののリストを次に示します。

  1. スタイルカスタマイズを保持するための埋め込みドキュメントを持つKudoモデルを追加します。
  2. ユーザーが作成者である称賛を定義する1:nの関係をUserモデルに追加します。 また、ユーザーが受信者である称賛を定義する同様の関係を追加します。
  3. 利用可能なオプションを定義するために、絵文字、部門、および色のenumを追加します。

注記:フィールドに@defaultを適用した後、コレクション内のレコードに新しい必須フィールドがない場合、次回読み取られるときにデフォルト値でそのフィールドを含めるように更新されます。

現時点では、更新する必要があるのはこれだけです。 npx prisma db pushを実行すると、PrismaClientが自動的に再生成されます。

ネストされたルート

フォームを保持するモーダルを作成するために、ネストされたルートを使用します。 これにより、定義したOutletで親ルートにレンダリングされるサブルートを設定できます。

ユーザーがこのネストされたルートに移動すると、ページ全体を再レンダリングすることなく、モーダルが画面にレンダリングされます。

ネストされたルートを作成するには、まずapp/routeshomeという名前の新しいフォルダーを追加します。

注記:そのフォルダーの名前付けは重要です。 home.tsxファイルがあるため、Remixは新しいhomeフォルダー内のすべてのファイルを/homeのサブルートとして認識します。

新しいapp/routes/homeディレクトリ内に、kudo.$userId.tsxという名前の新しいファイルを作成します。 これにより、モーダルコンポーネントを独自のルートであるかのように処理できます。

このファイル名の$userId部分は、ルートパラメーターであり、URLを介してアプリケーションに提供できる動的な値として機能します。 Remixは、そのファイル名をルートに変換します:/home/kudos/$userId。ここで、$userIdは任意の値にすることができます。

その新しいファイルで、動的な値が機能していることを確認するために、loader関数とテキストをレンダリングするReactコンポーネントをエクスポートします。

上記のコードは、いくつかのことを行います。

  1. loader関数からparamsフィールドをプルします。
  2. 次に、userId値を取得します。
  3. 最後に、RemixのuserLoaderDataフックを使用してloader関数からデータを取得し、userIdを画面にレンダリングします。

これはネストされたルートであるため、表示するには、ルートを親のどこに出力する必要があるかを定義する必要があります。

RemixのOutletコンポーネントを使用して、子ルートをapp/routes/home.tsxLayoutコンポーネントの直接の子としてレンダリングするように指定します。

http://localhost:3000/home/kudo/123にアクセスすると、「User: 123」というテキストがページの一番上に表示されるはずです。 URLの値を123以外に変更すると、その変更が画面に反映されるはずです。

IDでユーザーを取得する

ネストされたルートは機能していますが、userIdを使用してユーザーのデータを取得する必要があります。 app/utils/user.server.tsに、idに基づいて単一のユーザーを返す新しい関数を作成します。

上記のクエリは、指定されたidを持つデータベース内の一意のレコードを見つけます。 findUnique関数を使用すると、一意に識別するフィールド、またはデータベース内のそのレコードに対して一意である必要がある値を持つフィールドを使用してクエリをフィルタリングできます。

次に

  1. app/routes/home/kudo.$userId.tsxによってエクスポートされたloaderでその関数を呼び出します。
  2. json関数を使用して、そのloaderからの結果を返します。

次に、有効なidを持つネストされたルートに移動する方法が必要です。

ユーザーリストをレンダリングしているファイルであるapp/components/user-panel.tsxで、Remixが提供するuseNavigationフックをインポートし、ユーザーがクリックされたときにネストされたルートに移動するために使用します。

これで、ユーザーがそのパネル内の別のユーザーをクリックすると、そのユーザーの情報を含むサブルートに移動します。

それらがすべて問題ないように見える場合、次のステップはフォームを表示するモーダルコンポーネントの構築です。

ポータルを開く

このモーダルを構築するには、最初にポータルを作成するヘルパーコンポーネントを構築する必要があります。これにより、子コンポーネントを親のドキュメントオブジェクトモデル(DOM)ブランチの外側のどこかにレンダリングしながら、親コンポーネントが直接の子であるかのように管理できるようにします。

注記:このポータルは、モーダルの位置に影響を与える可能性のある親からの継承されたスタイルや位置を持たない場所にモーダルをレンダリングできるようにするため、重要になります。

app/componentsに、次の内容でportal.tsxという名前の新しいファイルを作成します。

このコンポーネントで何が起こっているかの説明を次に示します。

  1. idを持つdivを生成する関数が定義されています。 その要素は、ドキュメントのbodyにアタッチされます。
  2. 提供されたidを持つ要素がまだ存在しない場合は、createWrapper関数を呼び出して作成します。
  3. Portalコンポーネントがアンマウントされると、これは要素を破棄します。
  4. 新しく生成されたdivへのポータルを作成します。

結果として、このPortalでラップされた要素またはコンポーネントは、親の子として現在のDOMブランチではなく、bodyタグの直接の子としてレンダリングされます。

実際に動作することを確認するために試してみてください。 app/routes/home/kudos.$userId.tsxで、新しいPortalコンポーネントをインポートし、返されたコンポーネントでラップします。

ネストされたルートに移動すると、id"kudo-modal"divが、DOMツリー内でネストされたルートがレンダリングされている場所ではなく、bodyの直接の子としてレンダリングされることがわかります。

モーダルコンポーネントを構築する

安全なポータルができたので、モーダルコンポーネント自体の構築を開始します。 このアプリケーションには2つのモーダルがあるため、再利用可能な方法でコンポーネントを構築します。

app/components/modal.tsxに新しいファイルを作成します。 このファイルは、次のpropsを持つコンポーネントをエクスポートする必要があります。

  • children:モーダル内にレンダリングする要素。
  • isOpen:モーダルが表示されているかどうかを決定するフラグ。
  • ariaLabel:(オプション)ariaラベルとして使用される文字列。
  • className:(オプション)モーダルのコンテンツに追加のクラスを追加できる文字列。

次のコードを追加して、Modalコンポーネントを作成します。

Portalコンポーネントがインポートされ、モーダル全体をラップして、安全な場所にレンダリングされるようにします。

モーダルは、さまざまなTailwindCSSヘルパーを使用して、不透明な背景を持つ画面上の固定要素として定義されます。

背景(モーダル自体から離れた場所)がクリックされると、ユーザーは/homeルートに移動し、モーダルが閉じます。

フォームを構築する

app/routes/home/kudo.$userId.tsx 内で、新しい Modal コンポーネントをインポートし、現在レンダーされている Portal の代わりに Modal をレンダーしてください。

サイドパネルからユーザーがクリックされたときに、モーダルが開くようになるはずです。

フォームがメッセージのプレビューを表示する際にログインユーザーの情報が必要になるため、フォームを作成する前に、loader 関数からのレスポンスにそのデータを追加してください。

次に、そのファイル内の KudoModal 関数に以下の変更を加えてください。

これは新しいコードの大きな塊なので、どのような変更が加えられたかを見てみましょう。

  1. 必要なコンポーネントとフックをいくつかインポートします。
  2. フォームデータとエラーを処理するために必要なさまざまなフォーム変数を設定します。
  3. 入力の変更を処理する関数を作成します。
  4. 以前は <h2> タグだったものの代わりに、フォームコンポーネントの基本的なレイアウトをレンダーします。

ユーザーがkudoをカスタマイズできるようにする

このフォームでは、ユーザーがセレクトボックスを使用してカスタムスタイルを選択できるようにする必要があります。

app/components 内に select-box.tsx という名前の新しいファイルを作成し、SelectBox コンポーネントをエクスポートしてください。

このコンポーネントは、設定を受け取り、その状態を親によって管理できるようにする制御されたコンポーネントである点で、FormField コンポーネントに似ています。

これらのセレクトボックスには、色と絵文字のオプションを設定する必要があります。可能なオプションを保持するヘルパーファイルを app/utils/constants.ts に作成してください。

次に、app/routes/home/kudo.$userId.tsx で、SelectBox コンポーネントと定数をインポートします。また、それらをフォームの状態に接続し、{/* Select Boxes Go Here */} コメントの代わりに SelectBox コンポーネントをレンダーするために必要な変数と関数を追加してください。

これで、セレクトボックスがすべての可能なオプションとともに表示されるようになります。

kudo表示コンポーネントを追加する

このフォームには、ユーザーが受信者に表示されるコンポーネントの実際のレンダリングを確認できるプレビューセクションがあります。

app/componentskudo.tsx という名前の新しいファイルを作成してください。

このコンポーネントは、次のpropsを受け取ります。

  • profile: 受信者の user ドキュメントからの profile データ。
  • kudo: Kudo のデータとスタイリングオプション。

色と絵文字のオプションを持つ定数がインポートされ、カスタマイズされたスタイルをレンダーするために使用されます。

これで、このコンポーネントを app/routes/home/kudo.$userId.tsx にインポートし、{/* The Preview Goes Here */} コメントの代わりにレンダーできます。

プレビューがレンダーされ、現在ログインしているユーザーの情報と、送信しようとしているスタイル付きメッセージが表示されるようになります。

kudoを送信するアクションを作成する

フォームは視覚的に完成し、残りの唯一のピースはそれを機能させることです!

app/utilskudos.server.ts という名前の新しいファイルを作成し、kudoのクエリまたは保存に関連する関数を記述します。

このファイルで、kudoフォームデータ、作成者の id、および受信者の id を受け取る createKudo メソッドをエクスポートします。次に、Prismaを使用してそのデータを保存します。

上記のクエリは、次のことを行います。

  1. message 文字列と style 埋め込みドキュメントを渡します。
  2. 関数に渡されたIDを使用して、新しいkudoを適切な作成者受信者に接続します。

この新しい関数を app/routes/home/kudo.$userId.tsx ファイルにインポートし、フォームデータと createKudo 関数の呼び出しを処理する action 関数を作成します。

上記のスニペットの概要は次のとおりです。

  1. 新しい createKudo 関数、Prismaによって生成されたいくつかの型、Remixからの ActionFunction 型、および以前に作成した requireUserId 関数をインポートします。
  2. リクエストから必要なすべてのフォームデータとフィールドをプルアウトします。
  3. すべてのフォームデータを検証し、問題が発生した場合に表示される適切なエラーをフォームに送り返します。
  4. createKudo 関数を使用して、新しい kudo を作成します。
  5. ユーザーを /home ルートにリダイレクトし、モーダルを閉じます。

kudosフィードを作成する

ユーザーが互いにkudoを送信できるようになったので、/home ページのユーザーフィードにそれらのkudoを表示する方法が必要になります。

kudo表示コンポーネントはすでに作成したので、ホームページでkudoのリストを取得してレンダーするだけで済みます。

app/utils/kudos.server.ts で、getFilteredKudos という名前の新しい関数を作成してエクスポートします。

上記の関数は、いくつかの異なるパラメーターを受け取ります。それらは次のとおりです。

  • userId: クエリが取得する必要があるkudoのユーザーの id
  • sortFilter: 結果をソートするためにクエリの orderBy オプションに渡されるオブジェクト。
  • whereFilter: 結果をフィルタリングするためにクエリの where オプションに渡されるオブジェクト。

: Prismaは、上記で使用されている Prisma.KudoWhereInput など、クエリの一部を安全に型指定するために使用できる型を生成します。

次に、app/routes/home.tsx で、その関数をインポートし、loader 関数で呼び出します。また、Kudo コンポーネントと、Kudosのフィードをレンダーするために必要な型をインポートします。

Prismaによって生成された Kudo および Profile 型は、KudoWithProfile 型を作成するために結合されます。これは、配列に作成者からのプロファイルデータを含むkudoがあるため必要です。

アカウントにいくつかのkudoを送信してそのアカウントにログインすると、フィードにkudoのレンダーされたリストが表示されるはずです。

getFilteredKudos の呼び出しがソートおよびフィルターオプションに空のオブジェクトを提供していることに気付くかもしれません。これは、UIでフィードをフィルタリングまたはソートする方法がまだないためです。次に、これを処理するためにフィードの上部に検索バーを作成します。

app/componentssearch-bar.tsx という名前の新しいファイルを作成します。このコンポーネントは、ソートオブジェクトとフィルターオブジェクトの作成に使用されるクエリパラメータを渡して、/home ページにフォームを送信します。

上記のコードでは、テキストフィルターと検索パラメータの送信を処理するために、inputbutton が追加されました。

URLに filter 変数が存在する場合、ボタンは「検索」ボタンではなく「フィルターをクリア」ボタンに変わります。

そのファイルを app/routes/home.tsx にインポートし、{/* Search Bar Goes Here */} コメントの代わりにレンダーします。

これらの変更はフィードのフィルタリングを処理しますが、フィードをさまざまな列でソートすることも必要です。

app/utils/constants.ts で、利用可能な列を定義する sortOptions 定数を追加します。

次に、その定数と SelectBox コンポーネントを app/components/search-bar.tsx ファイルにインポートし、button 要素の直前にそれらのオプションを持つ SelectBox をレンダーします。

これで、オプションを含むドロップダウンが検索バーに表示されるはずです。

検索バーアクションを作成する

検索フォームが送信されると、フィルターとソートデータがURLに渡されて、/homeGET リクエストが送信されます。app/routes/home.tsx によってエクスポートされた loader 関数で、URLから sort および filter データをプルし、結果でクエリを作成します。

上記のコード

  1. URLパラメータをプルアウトします。
  2. URLに渡されたデータによって異なる可能性があるPrismaクエリに渡す sortOptions オブジェクトを作成します。
  3. URLに渡されたデータによって異なる可能性があるPrismaクエリに渡す textFilter オブジェクトを作成します。
  4. 新しいフィルターを含めるように getFilteredKudos の呼び出しを更新します。

これで、フォームを送信すると、フィードに結果が反映されるはずです!

最新のkudosを表示する

フィードに必要な最後のものは、最近送信されたkudosを最も多く表示する方法です。このコンポーネントは、kudosの最近の3人の受信者の UserCircle コンポーネントを表示します。

app/componentsrecent-bar.tsx という名前の新しいファイルを次のコードで作成します。

このコンポーネントは、最近のkudosのトップ3のリストを受け取り、それらをパネルにレンダーします。

次に、そのデータを取得するクエリを記述する必要があります。app/utils/kudos.server.ts に、次のクエリを返す getRecentKudos という名前の関数を追加します。

このクエリ

  1. 結果を createdAt降順にソートして、最新から最古のレコードを取得します。
  2. そのリストから最初の3つのみを取得して、最新の3つのドキュメントを取得します。

次に、次のことを行う必要があります。

  • RecentBar コンポーネントと getRecentKudos 関数を app/routes/home.tsx ファイルにインポートします。
  • そのファイルの loader 関数内で getRecentKudos を呼び出します。
  • {/* Recent Kudos Goes Here */} コメントの代わりに、ホームページに RecentBar をレンダーします。

これで、ホームページが完成し、アプリケーションで送信された最新の3つのkudosのリストが表示されるはずです!

まとめと今後の展望

この記事では、このアプリケーションの主要な機能を作成し、その過程で多くの概念を学びました。それには以下が含まれます。

  • Remixでのリダイレクト
  • リソースルートの使用
  • Prisma Clientを使用したデータのフィルタリングとソート
  • Prismaスキーマでの埋め込みドキュメントの使用
  • ... その他多数!

このシリーズの次のセクションでは、サイトのプロファイル設定セクションを作成し、プロファイル写真を管理するための画像アップロードコンポーネントを作成して、このアプリケーションを完成させます。

次回の投稿をお見逃しなく!

Prismaニュースレターに登録する