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スキーマでの埋め込みドキュメントの使用
... その他多数!
教育
次の投稿を見逃さないで!