MongoDB、Prisma、Remixを使ってフルスタックアプリケーションをゼロから構築する方法を学ぶシリーズの第3回へようこそ! 今回は、ユーザーの称賛フィードを表示し、他のユーザーに称賛を送ることができるアプリケーションの主要部分を構築します。
目次
- はじめに
- ホームルートを構築する
- ユーザーリストパネルを追加する
- ユーザー表示コンポーネントを構築する
- ログアウト機能を追加する
- 称賛を送る機能を追加する
- フォームを構築する
- 称賛表示コンポーネントを追加する
- 称賛を送るアクションを構築する
- 称賛フィードを構築する
- 検索バーを構築する
- 最新の称賛を表示する
- まとめと今後のステップ
はじめに
このシリーズの前回では、アプリケーションのサインインフォームとサインアップフォームを構築し、セッションベースの認証を実装しました。 また、ユーザーのプロファイルデータを保持する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
フィルターは、id
がuserId
パラメーターと一致するドキュメントを除外します。 これは、現在ログインしているユーザーを除くすべての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/components
にuser-circle.tsx
という名前の新しいファイルを作成し、次のコンポーネントを追加します。
このコンポーネントは、user
ドキュメントからprofile
データのみを渡すため、Prismaによって生成されたProfile
型を使用します。
また、クリックアクションを提供したり、スタイルをカスタマイズするための追加クラスを追加したりできる、いくつかの構成可能なオプションがあります。
app/components/user-panel.tsx
で、この新しいコンポーネントをインポートし、<p>Users go here</p>
をレンダリングする代わりに、各ユーザーに1つずつレンダリングします。
素晴らしい! これで、ユーザーはホームページの左側の素敵な列にレンダリングされます。 この時点でのサイドパネルの唯一の非機能的な部分は、サインアウトボタンです。
ログアウト機能を追加する
app/routes
にlogout.ts
という名前の別のリソースルートを追加します。これは、呼び出されたときにログアウトアクションを実行します。
このルートは、POSTとGETの2つの可能なアクションを処理します。
POST
:これは、このシリーズの前回で記述されたlogout
関数をトリガーします。GET
:GET
リクエストが行われた場合、ユーザーはホームページに送信されます。
app/components/user-panel.ts
のサインアウトボタンの周りに、送信時にこのルートにPOSTするform
を追加します。
これで、ユーザーはアプリケーションからサインアウトできるようになりました! POST
リクエストに関連付けられたセッションを持つユーザーはサインアウトされ、そのセッションは破棄されます。
称賛を送る機能を追加する
ユーザーリストのユーザーがクリックされると、フォームを提供するモーダルがポップアップ表示されるはずです。 このフォームを送信すると、称賛がデータベースに保存されます。
このフォームには、次の機能があります。
- 誰に称賛を送っているかの表示。
- ユーザーへのメッセージを記入できるテキストエリア。
- 投稿の背景色とテキスト色を選択できるスタイリングオプション。
- 投稿に絵文字を追加できる絵文字セレクター。
- 投稿がどのように見えるかの正確なプレビュー。
Prismaスキーマを更新する
スキーマでまだ定義されていない、保存および表示するデータポイントがいくつかあります。 変更する必要があるもののリストを次に示します。
- スタイルカスタマイズを保持するための埋め込みドキュメントを持つ
Kudo
モデルを追加します。 - ユーザーが作成者である称賛を定義する1:nの関係を
User
モデルに追加します。 また、ユーザーが受信者である称賛を定義する同様の関係を追加します。 - 利用可能なオプションを定義するために、絵文字、部門、および色の
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コンポーネントをエクスポートします。
上記のコードは、いくつかのことを行います。
loader
関数からparams
フィールドをプルします。- 次に、
userId
値を取得します。 - 最後に、Remixの
userLoaderData
フックを使用してloader
関数からデータを取得し、userId
を画面にレンダリングします。
これはネストされたルートであるため、表示するには、ルートを親のどこに出力する必要があるかを定義する必要があります。
RemixのOutlet
コンポーネントを使用して、子ルートをapp/routes/home.tsx
のLayout
コンポーネントの直接の子としてレンダリングするように指定します。
http://localhost:3000/home/kudo/123にアクセスすると、「User: 123」というテキストがページの一番上に表示されるはずです。 URLの値を123
以外に変更すると、その変更が画面に反映されるはずです。
IDでユーザーを取得する
ネストされたルートは機能していますが、userId
を使用してユーザーのデータを取得する必要があります。 app/utils/user.server.ts
に、id
に基づいて単一のユーザーを返す新しい関数を作成します。
上記のクエリは、指定されたid
を持つデータベース内の一意のレコードを見つけます。 findUnique
関数を使用すると、一意に識別するフィールド、またはデータベース内のそのレコードに対して一意である必要がある値を持つフィールドを使用してクエリをフィルタリングできます。
次に
app/routes/home/kudo.$userId.tsx
によってエクスポートされたloaderでその関数を呼び出します。json
関数を使用して、そのloaderからの結果を返します。
次に、有効な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>
タグだったものの代わりに、フォームコンポーネントの基本的なレイアウトをレンダーします。
ユーザーが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/components
に kudo.tsx
という名前の新しいファイルを作成してください。
このコンポーネントは、次のpropsを受け取ります。
profile
: 受信者のuser
ドキュメントからのprofile
データ。kudo
:Kudo
のデータとスタイリングオプション。
色と絵文字のオプションを持つ定数がインポートされ、カスタマイズされたスタイルをレンダーするために使用されます。
これで、このコンポーネントを app/routes/home/kudo.$userId.tsx
にインポートし、{/* The Preview Goes Here */}
コメントの代わりにレンダーできます。
プレビューがレンダーされ、現在ログインしているユーザーの情報と、送信しようとしているスタイル付きメッセージが表示されるようになります。
kudoを送信するアクションを作成する
フォームは視覚的に完成し、残りの唯一のピースはそれを機能させることです!
app/utils
に kudos.server.ts
という名前の新しいファイルを作成し、kudoのクエリまたは保存に関連する関数を記述します。
このファイルで、kudoフォームデータ、作成者の id
、および受信者の id
を受け取る createKudo
メソッドをエクスポートします。次に、Prismaを使用してそのデータを保存します。
上記のクエリは、次のことを行います。
message
文字列とstyle
埋め込みドキュメントを渡します。- 関数に渡されたIDを使用して、新しいkudoを適切な作成者と受信者に接続します。
この新しい関数を app/routes/home/kudo.$userId.tsx
ファイルにインポートし、フォームデータと createKudo
関数の呼び出しを処理する action
関数を作成します。
上記のスニペットの概要は次のとおりです。
- 新しい
createKudo
関数、Prismaによって生成されたいくつかの型、RemixからのActionFunction
型、および以前に作成したrequireUserId
関数をインポートします。 - リクエストから必要なすべてのフォームデータとフィールドをプルアウトします。
- すべてのフォームデータを検証し、問題が発生した場合に表示される適切なエラーをフォームに送り返します。
createKudo
関数を使用して、新しいkudo
を作成します。- ユーザーを
/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/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を最も多く表示する方法です。このコンポーネントは、kudosの最近の3人の受信者の UserCircle
コンポーネントを表示します。
app/components
に recent-bar.tsx
という名前の新しいファイルを次のコードで作成します。
このコンポーネントは、最近のkudosのトップ3のリストを受け取り、それらをパネルにレンダーします。
次に、そのデータを取得するクエリを記述する必要があります。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スキーマでの埋め込みドキュメントの使用
- ... その他多数!
このシリーズの次のセクションでは、サイトのプロファイル設定セクションを作成し、プロファイル写真を管理するための画像アップロードコンポーネントを作成して、このアプリケーションを完成させます。
次回の投稿をお見逃しなく!
Prismaニュースレターに登録する