MongoDB、Prisma、Remix を使用してフルスタックアプリケーションをゼロから構築する方法を学ぶこのシリーズの第3弾へようこそ!このパートでは、ユーザーの Kudos フィードを表示し、他のユーザーに Kudos を送信できるようにするアプリケーションの主要部分を構築します。
目次
- はじめに
- ホームルートを構築する
- ユーザーリストパネルを追加する
- ユーザー表示コンポーネントを構築する
- ログアウト機能を追加する
- Kudos 送信機能を追加する
- フォームを構築する
- Kudo表示コンポーネントを追加する
- Kudos送信アクションを構築する
- Kudosフィードを構築する
- 検索バーを構築する
- 最新のKudosを表示する
- まとめと次へ
はじめに
このシリーズの前回のパートでは、アプリケーションのサインインとサインアップフォームを構築し、セッションベースの認証を実装しました。また、ユーザーのプロファイルデータを保持する新しい埋め込みドキュメントを User モデルに追加するために Prisma スキーマを更新しました。
このパートでは、アプリケーションの主要機能である Kudos フィードを構築します。各ユーザーは、他のユーザーから送信された Kudos のフィードを持ちます。また、ユーザーは他のユーザーに Kudos を送信できます。
さらに、フィード内のKudosを見つけやすくするために、検索とフィルタリングを実装します。
このプロジェクトの開始点は、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 フィルターは、id が userId パラメータと一致するドキュメントを除外します。これは、現在ログインしているユーザーを除くすべての user を取得するために使用されます。
注:埋め込みドキュメント内のフィールドでソートするのがいかに簡単か気づきましたか?
app/routes/home.tsx で、その新しい関数をインポートし、loader 内でそれを呼び出します。その後、Remix の json ヘルパーを使用してユーザーリストを返します。
注:
loader関数内で実行されるコードは、クライアントサイドのコードには公開されません。Remix のこの素晴らしい機能に感謝です!
データベースにユーザーがいて、ローダー内で users 変数を出力した場合、あなた自身を除くすべてのユーザーのリストが表示されるはずです。

注:
profile埋め込みドキュメント全体が、明示的に含めることなくネストされたオブジェクトとして取得されました。
これでデータが利用可能になりました。次は、それをどうするかです!
ユーザーをユーザーパネルに提供する
UserPanel コンポーネントに新しい users プロップを設定します。
ここで使用されている User 型は Prisma によって生成され、Prisma Client 経由で利用可能です。Remix は Prisma と非常にうまく連携します。なぜなら、フルスタックフレームワークでエンドツーエンドの型安全性を実現するのが非常に簡単だからです。
注:エンドツーエンドの型安全性とは、データの形状が変化しても、スタック全体の型が同期して維持されることです。
app/routes/home.tsx では、UserPanel コンポーネントにユーザーを提供できるようになりました。Remix が提供する loader 関数から返されたデータにアクセスできる useLoaderData フックをインポートし、それを使用して users データにアクセスします。
これでコンポーネントは users を操作できるようになりました。次はそれを表示する必要があります。
ユーザー表示コンポーネントを構築する
リストアイテムは、今のところユーザーの名と姓の最初の文字を持つ円として表示されます。
app/components に user-circle.tsx という名前の新しいファイルを作成し、以下のコンポーネントを追加します。
このコンポーネントは、Prisma によって生成された Profile 型を使用します。なぜなら、user ドキュメントから profile データのみを渡すからです。
また、クリックアクションを提供したり、スタイルをカスタマイズするための追加クラスを追加したりできる、いくつかの設定可能なオプションも備えています。
app/components/user-panel.tsx で、この新しいコンポーネントをインポートし、<p>Users go here</p> をレンダリングする代わりに、各ユーザーに対して1つずつレンダリングします。
素晴らしい!これでユーザーはホームページの左側に素敵な列でレンダリングされます。この時点でのサイドパネルで機能しない唯一の部分はサインアウトボタンです。

ログアウト機能を追加する
app/routes に logout.ts という別のリソースルートを追加します。これは、呼び出されるとログアウトアクションを実行します。
このルートは2つの可能なアクションを処理します:POSTとGET
POST: これは、このシリーズの前回のパートで記述されたlogout関数をトリガーします。GET:GETリクエストが行われた場合、ユーザーはホームページに送られます。
app/components/user-panel.ts 内のサインアウトボタンの周りに form を追加し、送信時にこのルートにポストするようにします。
これでユーザーはアプリケーションからサインアウトできます!セッションが POST リクエストに関連付けられているユーザーはサインアウトされ、セッションは破棄されます。
Kudos 送信機能を追加する
ユーザーリストのユーザーをクリックすると、フォームを提供するモーダルが表示されるようにします。このフォームを送信すると、Kudoがデータベースに保存されます。
このフォームには以下の機能があります
- Kudosを贈るユーザーの表示。
- ユーザーへのメッセージを記入できるテキストエリア。
- 投稿の背景色と文字色を選択できるスタイリングオプション。
- 投稿に絵文字を追加できる絵文字セレクター。
- 投稿がどのように見えるかの正確なプレビュー。
Prisma スキーマを更新する
まだスキーマに定義されていない、保存および表示するデータポイントがいくつかあります。変更する必要があるもののリストは次のとおりです。
- スタイルカスタマイズを保持するための埋め込みドキュメントを持つ
Kudoモデルを追加します。 Userモデルに、ユーザーが作成者であるKudosを定義する1:nリレーションを追加します。また、ユーザーが受信者であるKudosを定義する同様のリレーションも追加します。- 絵文字、部署、色に利用可能なオプションを定義するために
enumを追加します。
注: フィールドに
@defaultを適用した後、コレクション内のレコードに新しい必須フィールドがない場合、次回読み込まれるときにデフォルト値を含むように更新されます。
今のところ更新が必要なのはこれだけです。npx prisma db push を実行すると、PrismaClient が自動的に再生成されます。
ネストされたルート
フォームを保持するモーダルを作成するには、ネストされたルートを使用します。これにより、定義した Outlet で親ルートにレンダリングされるサブルートを設定できます。
ユーザーがこのネストされたルートに移動すると、ページ全体を再レンダリングすることなく、モーダルが画面にレンダリングされます。
ネストされたルートを作成するには、まず app/routes に home という名前の新しいフォルダを追加します。
注:このフォルダの名前付けは重要です。
home.tsxファイルがあるため、Remix は新しいhomeフォルダ内の任意のファイルを/homeのサブルートとして認識します。
新しい app/routes/home ディレクトリ内に、kudo.$userId.tsx という名前の新しいファイルを作成します。これにより、モーダルコンポーネントをそれ自体のルートであるかのように扱えるようになります。

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

その新しいファイルで、loader 関数と、動的な値が機能していることを確認するためにテキストをレンダリングする React コンポーネントをエクスポートします。
上記のコードはいくつかのことを行います。
- ローダー関数から
paramsフィールドを取得します。 - 次に、
userIdの値を取得します。 - 最後に、Remix の
userLoaderDataフックを使用してloader関数からデータを取得し、userIdを画面にレンダリングします。
これはネストされたルートであるため、表示するには、親でルートが出力される場所を定義する必要があります。
Remix の Outlet コンポーネントを使用して、app/routes/home.tsx の Layout コンポーネントの直接の子として子ルートがレンダリングされるように指定します。
https://:3000/home/kudo/123 にアクセスすると、「User: 123」というテキストがページの一番上に表示されるはずです。URL の値を 123 以外のものに変更すると、その変更が画面に反映されるはずです。

ID でユーザーをフェッチする
ネストされたルートは機能していますが、userId を使用してユーザーのデータを取得する必要があります。app/utils/user.server.ts に、id に基づいて単一のユーザーを返す新しい関数を作成します。
上記のクエリは、指定された id を持つデータベース内のユニークなレコードを検索します。findUnique 関数を使用すると、ユニークに識別可能なフィールド、つまりデータベース内でそのレコードに必ずユニークな値を持つフィールドを使用してクエリをフィルタリングできます。
次に
app/routes/home/kudo.$userId.tsxによってエクスポートされたローダー内でその関数を呼び出します。json関数を使用して、そのローダーの結果を返します。
次に、有効な id を持つネストされたルートに移動する方法が必要です。
ユーザーリストをレンダリングしているファイル app/components/user-panel.tsx で、Remix が提供する useNavigation フックをインポートし、ユーザーがクリックされたときにネストされたルートに移動するために使用します。
これで、ユーザーがパネル内の別のユーザーをクリックすると、そのユーザーの情報を含むサブルートに移動します。

すべてが順調に見えるなら、次のステップはフォームを表示するモーダルコンポーネントを構築することです。
ポータルを開く
このモーダルを構築するには、まず ポータル を作成するヘルパーコンポーネントを構築する必要があります。ポータルを使用すると、子コンポーネントを親のドキュメントオブジェクトモデル (DOM) ブランチの外にレンダリングできますが、親コンポーネントはそれを直接の子であるかのように管理できます。

注:このポータルは重要です。なぜなら、モーダルの位置に影響を与える可能性のある、親から継承されたスタイルや位置を持たない場所にモーダルをレンダリングできるからです。
app/components に portal.tsx という名前の新しいファイルを以下の内容で作成します。
このコンポーネントで行われていることの説明は次のとおりです。
idを持つdivを生成する関数が定義されます。その要素はドキュメントのbodyにアタッチされます。- 指定された
idを持つ要素がまだ存在しない場合、createWrapper関数を呼び出して作成します。 Portalコンポーネントがアンマウントされると、この要素は破棄されます。- 新しく生成された
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 関数に以下の変更を加えます。
- これは大量の新しいコードだったので、どのような変更が加えられたか見てみましょう。
- 必要なコンポーネントとフックをいくつかインポートします。
- フォームデータとエラーを処理するために必要なさまざまなフォーム変数を設定します。
- 入力の変更を処理する関数を作成します。
<h2> タグがあった場所にフォームコンポーネントの基本的なレイアウトをレンダリングします。
このフォームでは、ユーザーが選択ボックスを使用してカスタムスタイルを選択できるようにする必要があります。
app/components に select-box.tsx という名前の新しいファイルを作成し、SelectBox コンポーネントをエクスポートします。
このコンポーネントは FormField コンポーネントに似ており、構成を受け取り、親によって状態が管理される制御されたコンポーネントです。
これらの選択ボックスには、色と絵文字のオプションを入力する必要があります。app/utils/constants.ts に、可能なオプションを保持するヘルパーファイルを作成します。
次に、app/routes/home/kudo.$userId.tsx で、SelectBox コンポーネントと定数をインポートします。また、それらをフォームの状態に接続し、{/* Select Boxes Go Here */} コメントの代わりに SelectBox コンポーネントをレンダリングするために必要な変数と関数を追加します。

これで、考えられるすべてのオプションが選択ボックスに表示されるようになります。
このフォームには、ユーザーが受信者が実際に目にするコンポーネントのレンダリングを確認できるプレビューセクションがあります。
app/components に kudo.tsx という名前の新しいファイルを作成します。
- このコンポーネントはプロップを受け取ります。
profile: 受信者のuserドキュメントからのprofileデータ。
kudo: Kudo のデータとスタイリングオプション。
色と絵文字のオプションを持つ定数がインポートされ、カスタマイズされたスタイルをレンダリングするために使用されます。
これで、このコンポーネントを app/routes/home/kudo.$userId.tsx にインポートし、{/* The Preview Goes Here */} コメントの代わりにレンダリングできます。

プレビューがレンダリングされ、現在ログインしているユーザーの情報と送信しようとしているスタイル付きメッセージが表示されます。
これでフォームは視覚的に完成し、残るは機能させるだけです!
app/utils に kudos.server.ts という新しいファイルを作成し、Kudos のクエリや保存に関連する関数を記述します。
このファイルで、Kudo フォームデータ、作成者の id、受信者の id を受け取る createKudo メソッドをエクスポートします。その後、Prisma を使用してそのデータを保存します。
- 上記のクエリは以下のことを行います。
message文字列とstyle埋め込みドキュメントを渡します。
関数に渡された ID を使用して、新しい Kudos を適切な作成者と受信者に接続します。
この新しい関数を app/routes/home/kudo.$userId.tsx ファイルにインポートし、フォームデータと createKudo 関数の呼び出しを処理する action 関数を作成します。
- 上記のコードの概要は次のとおりです。
- 新しい
createKudo関数、Prisma によって生成されたいくつかの型、Remix のActionFunction型、および以前に記述したrequireUserId関数をインポートします。 - リクエストから必要なフォームデータとフィールドをすべて抽出します。
- すべてのフォームデータを検証し、問題が発生した場合は適切なエラーをフォームに送り返して表示します。
createKudo関数を使用して新しいkudoを作成します。
ユーザーを /home ルートにリダイレクトし、モーダルを閉じます。
これでユーザーがKudosを送り合えるようになったので、/home ページでそれらのKudosをユーザーのフィードに表示する方法が必要になります。
Kudo 表示コンポーネントはすでに構築済みなので、ホームページで Kudos のリストを取得してレンダリングするだけです。
app/utils/kudos.server.ts に getFilteredKudos という新しい関数を作成し、エクスポートします。
- 上記の関数はいくつかの異なるパラメーターを受け取ります。それらは次のとおりです。
userId: クエリがKudosを取得するユーザーのid。sortFilter: 結果をソートするためにクエリのorderByオプションに渡されるオブジェクト。
whereFilter: 結果をフィルタリングするためにクエリのwhereオプションに渡されるオブジェクト。
注:Prisma は、上記の Prisma.KudoWhereInput のように、クエリの各部分を安全に型付けするために使用できる型を生成します。
次に、app/routes/home.tsx で、その関数をインポートし、loader 関数内で呼び出します。また、Kudo コンポーネントと、Kudos のフィードをレンダリングするために必要な型をインポートします。
Prisma によって生成された Kudo 型と Profile 型は結合されて KudoWithProfile 型を作成します。これは、配列に作者のプロファイルデータを含むKudosが含まれているため、必要です。

いくつか Kudos をアカウントに送信し、そのアカウントにログインすると、フィードに Kudos のリストがレンダリングされて表示されるはずです。
getFilteredKudos の呼び出しで、ソートおよびフィルターオプションに空のオブジェクトが提供されていることに気づくかもしれません。これは、UI にフィードをフィルターまたはソートする方法がまだないためです。次に、これを処理するためにフィードの先頭に検索バーを作成します。
app/components に search-bar.tsx という名前の新しいファイルを作成します。このコンポーネントは /home ページにフォームを送信し、必要なソートオブジェクトとフィルターオブジェクトを構築するために使用されるクエリパラメータを渡します。
上記のコードでは、テキストフィルターと検索パラメータの送信を処理するために input と button が追加されています。
URLに filter 変数が存在する場合、ボタンは「検索」ボタンではなく「フィルターをクリア」ボタンに変わります。

そのファイルを app/routes/home.tsx にインポートし、{/* Search Bar Goes Here */} コメントの代わりにレンダリングします。
これらの変更はフィードのフィルタリングを処理しますが、さまざまな列でフィードをソートすることもできます。
app/utils/constants.ts に、利用可能な列を定義する sortOptions 定数を追加します。
次に、その定数と SelectBox コンポーネントを app/components/search-bar.tsx ファイルにインポートし、button 要素の直前にこれらのオプションを持つ SelectBox をレンダリングします。

これで、検索バーにオプションのドロップダウンが表示されるはずです。
検索フォームが送信されると、フィルターデータとソートデータが URL を介して渡され、/home への GET リクエストが行われます。app/routes/home.tsx によってエクスポートされた loader 関数で、URL から sort および filter データを取得し、結果でクエリを構築します。
- 上記のコードは
- URLパラメータを抽出します。
- URLで渡されたデータに応じて変化する可能性のある、Prismaクエリに渡す
sortOptionsオブジェクトを構築します。 - URLで渡されたデータに応じて変化する可能性のある、Prismaクエリに渡す
textFilterオブジェクトを構築します。
getFilteredKudos の呼び出しを新しいフィルターを含むように更新します。

これでフォームを送信すると、フィードに結果が反映されるはずです!
フィードに必要な最後のものは、最も最近送信された Kudos を表示する方法です。このコンポーネントは、Kudos の最も最近の3人の受信者に対して UserCircle コンポーネントを表示します。
app/components に recent-bar.tsx という名前の新しいファイルを以下のコードで作成します。
このコンポーネントは、上位3つの最近のKudosのリストを受け取り、それらをパネルにレンダリングします。
次に、そのデータを取得するクエリを記述する必要があります。app/utils/kudos.server.ts に getRecentKudos という名前の関数を追加し、以下のクエリを返します。
- このクエリは
- 結果を
createdAtで降順にソートし、新しいものから古いものへとレコードを取得します。
そのリストから最初の3つだけを取得し、最も新しい3つのドキュメントを取得します。
- 次に、以下のことを行う必要があります。
RecentBarコンポーネントとgetRecentKudos関数をapp/routes/home.tsxファイルにインポートします。- そのファイルの
loader関数内でgetRecentKudosを呼び出します。
{/* Recent Kudos Goes Here */} コメントの代わりに、RecentBar をホームページにレンダリングします。

これでホームページが完成し、アプリケーションで送信された最新の3つのKudosのリストが表示されるはずです!
- この記事では、このアプリケーションの主要な機能を構築し、その過程で多くの概念を学びました。
- Remixでのリダイレクト
- リソースルートの使用
- Prisma Clientによるデータのフィルタリングとソート
- Prismaスキーマでの埋め込みドキュメントの使用
... その他多数!
教育
次の投稿を見逃さないで!