📑

URLクエリパラメータを使ったタブ切り替えの Tips [React Server Component]

2023/12/10に公開

はじめに 🚩

この記事では、URL クエリパラメータを使ってタブ操作とページ遷移を行う方法について説明します。具体的には、Next.js の App Router 用いて Server Component の実装例を通した Tips を解説します。

実装例 📝

タブに関するコンポーネントはスタイリングソリューションとして、shadcn/ui を使用します。

https://ui.shadcn.com/docs/components/tabs

タブの状態管理

useState を使用する方法と、URL クエリパラメータを使用する方法の2つのパターンでタブの状態管理について説明します。
useState を使用した場合は、以下のようになります。

'use client';

export default function Page() {
  const [tabValue, setTabValue] = useState('tab1');

  return (
    <Tabs value={tabValue}>
      <TabsList>
        <TabsTrigger value='tab1' onClick={() => setTabValue('tab1')}>
          tab1
        </TabsTrigger>
        <TabsTrigger value='tab2' onClick={() => setTabValue('tab2')}>
          tab2
        </TabsTrigger>
        <TabsTrigger value='tab3' onClick={() => setTabValue('tab3')}>
          tab3
        </TabsTrigger>
      </TabsList>
      <TabsContent value='tab1'>
        <p>tab1</p>
      </TabsContent>
      <TabsContent value='tab2'>
        <p>tab2</p>
      </TabsContent>
      <TabsContent value='tab3'>
        <p>tab3</p>
      </TabsContent>
    </Tabs>
  );
}

この場合、ブラウザをリロードするとタブの状態がリセットされてしまいます。これは、useState のデフォルト値が常に初期値になるためです。

一方、URL クエリパラメータを使用した場合は、以下のようになります。

589f7a84c469f8

export default function Page({
  searchParams,
}: {
  searchParams: {
    [key: string]: string | string[] | undefined;
  };
}) {
  const tabValue = (searchParams.value as string) || 'tab1';

  return (
    <Tabs defaultValue={tabValue} className='h-full space-y-6'>
      <TabsList>
        <Link href='?value=tab1'>
          <TabsTrigger value='tab1'>tab1</TabsTrigger>
        </Link>
        <Link href='?value=tab2'>
          <TabsTrigger value='tab2'>tab2</TabsTrigger>
        </Link>
        <Link href='?value=tab3'>
          <TabsTrigger value='tab3'>tab3</TabsTrigger>
        </Link>
      </TabsList>
      <TabsContent value='tab1'>
        <p>tab1</p>
      </TabsContent>
      <TabsContent value='tab2'>
        <p>tab2</p>
      </TabsContent>
      <TabsContent value='tab3'>
        <p>tab3</p>
      </TabsContent>
    </Tabs>
  );
}

クエリパラメータとしてタブの状態を管理することで、ブラウザをリロードしてもタブの状態を保持することができます。
これにより、以下の図解のように、詳細ページでリロードした場合でも、タブの状態を保持することができます。

589f7a84c469f8

コード例
page.tsx
import React from 'react';
import Link from 'next/link';

import { buttonVariants } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

export default function Page({
  searchParams,
}: {
  searchParams: {
    [key: string]: string | string[] | undefined;
  };
}) {
  const tabValue = (searchParams.value as string) || 'tab1';

  return (
    <div className='grid h-full gap-2 px-4 py-6 lg:px-8'>
      <h1 className='text-2xl font-bold'>ルートページ</h1>
      <Tabs defaultValue={tabValue} className='h-full space-y-6'>
        <div className='flex items-center justify-between'>
          <TabsList>
            <Link href='?value=tab1'>
              <TabsTrigger value='tab1' className='relative'>
                tab1
              </TabsTrigger>
            </Link>
            <Link href='?value=tab2'>
              <TabsTrigger value='tab2'>tab2</TabsTrigger>
            </Link>
            <Link href='?value=tab3'>
              <TabsTrigger value='tab3'>tab3</TabsTrigger>
            </Link>
          </TabsList>
        </div>
        <TabsContent
          value='tab1'
          className='h-full flex-col border-none px-2 data-[state=active]:flex'
        >
          <p>tab1</p>
          <DetailLink tabValue={tabValue} />
        </TabsContent>
        <TabsContent
          value='tab2'
          className='h-full flex-col border-none px-2 data-[state=active]:flex'
        >
          <p>tab2</p>
          <DetailLink tabValue={tabValue} />
        </TabsContent>
        <TabsContent
          value='tab3'
          className='h-full flex-col border-none px-2 data-[state=active]:flex'
        >
          <p>tab3</p>
          <DetailLink tabValue={tabValue} />
        </TabsContent>
      </Tabs>
    </div>
  );
}

function DetailLink({ tabValue }: { tabValue: string }) {
  return (
    <Link
      href={`/${1}?value=${tabValue}`}
      className={buttonVariants({
        variant: 'ghost',
        size: 'sm',
        className: 'w-fit hover:bg-blue-50',
      })}
    >
      <span className='text-sm text-blue-600'>詳細ページへ遷移</span>
    </Link>
  );
}

app/[id]/page.tsx
import React from 'react';
import Link from 'next/link';
import { ChevronLeft } from 'lucide-react';

interface DetailsPageProps {
  params: {
    id: string;
  };
  searchParams: {
    [key: string]: string | string[] | undefined;
  };
}

export default function DetailsPage({
  params,
  searchParams,
}: DetailsPageProps) {
  const tab = searchParams.tab as string;

  return (
    <div className='grid h-full gap-2 px-4 py-6 lg:px-8'>
      <div className='flex items-center gap-2'>
        <Link
          href={`/?value=${tab}`}
          className='rounded-md hover:bg-accent hover:text-accent-foreground'
        >
          <ChevronLeft size={24} />
        </Link>
        <h1 className='text-2xl font-bold'>詳細ページ</h1>
      </div>
      <div className='flex items-start space-x-4'>{tab}の詳細</div>
    </div>
  );
}

また、URL クエリパラメータを使用することで、ユーザーが URL を共有することで、他の人が同じビューを見ることができるようになります。

589f7a84c469f8

さらに、Server Component で完結するので、クライアントサイドのレンダリング負荷を軽減し、初期ロード時間を短縮できるため実行時のパフォーマンスも向上します。

まとめ 📌

URL クエリパラメータを使用したタブの状態管理には、以下のような利点があります。

  • ブラウザの「戻る」や「進む」ボタンを使って、異なる状態間を簡単にナビゲートできる
  • ユーザーが URL を共有することで、他の人が同じビューを見ることができる
  • ブックマーク機能を使って、特定の状態のページに簡単にアクセスできる
  • Server Component を使用することで、クライアントサイドのレンダリング負荷を軽減し、初期ロード時間を短縮できる

useState を使用した Client Component によるタブの状態管理だとリロードした場合にタブの状態がリセットされてしまうため、(ケースにもよりますが)ユーザーにとって不便な UX となります。
そのため、URL クエリパラメータを使用した(Server Component による)タブの状態管理を推奨します。

以上です!

参考 📚

https://nextjs.org/docs/app/api-reference/file-conventions/page

Discussion