URLクエリパラメータを使ったタブ切り替えの Tips [React Server Component]
はじめに 🚩
この記事では、URL クエリパラメータを使ってタブ操作とページ遷移を行う方法について説明します。具体的には、Next.js の App Router 用いて Server Component の実装例を通した Tips を解説します。
実装例 📝
タブに関するコンポーネントはスタイリングソリューションとして、shadcn/ui を使用します。
タブの状態管理
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 クエリパラメータを使用した場合は、以下のようになります。
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>
);
}
クエリパラメータとしてタブの状態を管理することで、ブラウザをリロードしてもタブの状態を保持することができます。
これにより、以下の図解のように、詳細ページでリロードした場合でも、タブの状態を保持することができます。
コード例
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>
);
}
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 を共有することで、他の人が同じビューを見ることができるようになります。
さらに、Server Component で完結するので、クライアントサイドのレンダリング負荷を軽減し、初期ロード時間を短縮できるため実行時のパフォーマンスも向上します。
まとめ 📌
URL クエリパラメータを使用したタブの状態管理には、以下のような利点があります。
- ブラウザの「戻る」や「進む」ボタンを使って、異なる状態間を簡単にナビゲートできる
- ユーザーが URL を共有することで、他の人が同じビューを見ることができる
- ブックマーク機能を使って、特定の状態のページに簡単にアクセスできる
- Server Component を使用することで、クライアントサイドのレンダリング負荷を軽減し、初期ロード時間を短縮できる
useState を使用した Client Component によるタブの状態管理だとリロードした場合にタブの状態がリセットされてしまうため、(ケースにもよりますが)ユーザーにとって不便な UX となります。
そのため、URL クエリパラメータを使用した(Server Component による)タブの状態管理を推奨します。
以上です!
参考 📚
Discussion