👨‍👦

Remix Nested RoutesでIndeed風のUIを作る

2023/12/22に公開

Remixを使って、以下のようなIndeed風求人サイトのUIを作成してみます。

1. ルーティングの設定

app/
├── routes/
│   ├── _index.tsx
│   ├── joblist.tsx 
│   ├── joblist.$jobId.tsx            
└── root.tsx
│
└── data.ts

求人一覧を表示するルート(joblist.tsx)とその子要素として求人詳細を表示するルート(joblist.$jobId.tsx)を設定します。
親子関係を作ることで、親要素のレイアウトを受け継ぐことができます。
※親要素に<Outlet />を忘れると子要素が表示されないので注意

data.ts には、開発用のテストデータとそのテストデータを取得する関数を作成しています。

2. 求人一覧の表示

まずloader関数でdata.tsで作成したgetJobPostings関数を呼び出し、テストデータを全て取得します。

// app/routes/joblist.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { NavLink, Outlet, useLoaderData } from "@remix-run/react";
import { getJobPostings } from "../data";

export const loader = async () => {
  const jobPostings = await getJobPostings();
  return json({ jobPostings });
};

// 省略...

useLoaderDataでloader関数で取得したデータを返します。
jobPostings配列の中に各求人がオブジェクト形式で入っているため、mapで展開します。
各求人はNavLinkを使用し、クリックすると求人の詳細ページへ遷移するようにします(to={posting.id})。

// 続き...
export default function JobList() {
  const {jobPostings} = useLoaderData<typeof loader>();
  return (
{/* 省略 */}
<div className="w-2/5">
  {jobPostings.map((posting) => (
    <NavLink 
	key={posting.id}
	preventScrollReset
	to={posting.id}
	className={({ isActive }) =>
	 `hover:shadow-lg cursor-pointer p-8 mb-3 w-full rounded-md bg-white block  
	 ${isActive 
	 ? "border-2 border-blue-500" 
	 : "border border-gray-300"} `}
    >
      <h2>{posting.title}</h2>
      {/* その他の情報表示 */}
    </NavLink>
  ))}
</div>

NavLinkはclassNameコールバックを使うことで、リンクのアクティブ状態やペンディング状態に応じて動的にクラスを適用することができます。
現在選択している求人が何かわかりやすいように、今回はisActiveの時に枠線を青色にしています。

3. UIのデザイン

求人一覧は左側に配置し、選択された求人の詳細は右側に表示されるようにします。
今回はtailwindcssを使用しているので、親要素にflexを当てることで一覧と詳細ページを横に並べます。
<Outlet />にそのルートの子要素(今回は/joblist.$jobId.tsx)が配置されます。

<div className="flex justify-between my-5">
  {/* 求人一覧 */}
  <div className="w-2/5"> 
   {/* ... */} 
  </div>

  {/* 求人詳細 */}
  <div className="w-7/12">
    <div className="sticky top-5 pb-3">
      <Outlet />
    </div>
  </div>
</div>

詳細ページにclasName="sticky top-num"を当てることで、一覧画面のスクロールに追尾するようにします。

4. 求人詳細ページ

ユーザーが求人をクリックすると、ネストされたルートである(joblist.$jobId.tsx)に遷移します。
URLのjobIdには各求人のposting.idが入ります。
ここでもloader関数を使用して、data.tsで作成したgetJobPosting関数(引数に特定のidを持つ)を呼び出し、idと該当する求人のデータを取得します。

// app/routes/joblist.$jobId.tsx
import { json, LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { getJobPosting } from "../data";

export const loader = async ({
  params,
}: LoaderFunctionArgs) => {
  invariant(params.jobId, "Missing jobId param");
  const posting = await getJobPosting(params.jobId);
   if (!posting) {
    throw new Response("Not Found", { status: 404 });
  }
  return json({ posting });
};

// 省略...

URLの$jobIdに当たる動的な値は、loader関数のparams引数でアクセスすることができます。
今回は$jobIdのため、params.jobIdでURLパラメータの値を取得できます。

invariant関数を使用することで、条件が偽(false)の場合にエラーを投げることができます。

また、404エラーの処理を含めないと、Typescriptがアラートを出してくるので、if(!posting)でエラー処理を追加します。

これでIndeed風の求人一覧・詳細画面を作成することができます!

Discussion