Next.js Linkコンポーネントの業務実装パターン|実務で使える最適化テクニック
はじめに
Next.jsのLinkコンポーネントは、シンプルなクライアント側ナビゲーション機能に見えますが、実務でこれを効果的に活用するには多くの判断が必要です。本記事では、実際の案件で頻出するパターンを中心に、その実装方法と注意点を解説します。
1. Next.js Linkコンポーネント基礎解説
Next.jsのLinkコンポーネントは、ページ間をクライアント側でナビゲートするための機能です。従来のアンカータグと異なり、ページリロードなしに遷移でき、パフォーマンスが大幅に改善されます。
基本的な使い方は以下の通りです:
import Link from 'next/link';
export default function Navigation() {
return (
<Link href=\"/about\">
About Page
</Link>
);
}
Next.js 13以降では、Linkコンポーネントの仕様が変更され、childrenに自動的にpropが付与されるようになりました。これにより、より柔軟な実装が可能になっています。
2. 業務で頻出するユースケース
2.1 認証状態に基づくリンク制御
実務では、ユーザーの認証状態によってリンク先を変更する必要があるケースが多いです。例えば、未認証ユーザーはログインページへ、既認証ユーザーはダッシュボードへ遷移させるなどです。
2.2 クエリパラメータを含むリンク
検索結果や一覧フィルタなど、動的なパラメータをURLに含める必要があります。
2.3 プリフェッチの最適化
パフォーマンスを考慮して、不要なプリフェッチを制御する必要があります。
2.4 外部リンクと内部リンクの分岐
内部リンクはLinkコンポーネント、外部リンクは通常のアンカータグを使い分ける必要があります。
3. 実装コード|実務パターン集
3.1 認証ベースのリンク制御パターン
ユーザー認証状態に基づいてリンク遷移を制御する実装は、実務では非常に多いです。useSessionやuseAuthなどのカスタムフックと組み合わせることで、柔軟に実装できます。
// utils/authLink.ts
import { useSession } from 'next-auth/react';
export const useAuthLink = () => {
const { data: session } = useSession();
const getRedirectPath = (publicPath: string, privatePath: string) => {
return session ? privatePath : publicPath;
};
return { getRedirectPath };
};
// components/NavLink.tsx
'use client';
import Link from 'next/link';
import { useSession } from 'next-auth/react';
interface NavLinkProps {
publicPath: string;
privatePath: string;
children: React.ReactNode;
className?: string;
}
export const ProtectedLink: React.FC<NavLinkProps> = ({
publicPath,
privatePath,
children,
className,
}) => {
const { data: session } = useSession();
const href = session?.user ? privatePath : publicPath;
return (
<Link href={href} className={className}>
{children}
</Link>
);
};
// 使用例
export default function Header() {
return (
<nav>
<ProtectedLink
publicPath=\"/login\"
privatePath=\"/dashboard\"
className=\"nav-item\"
>
Dashboard
</ProtectedLink>
</nav>
);
}
3.2 クエリパラメータ付きリンク
検索フィルタやページング、ソート順序などをURLパラメータで管理する場合、以下のようなアプローチが実務では効果的です。
// components/ProductList.tsx
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
interface FilterParams {
category?: string;
sort?: 'asc' | 'desc';
page?: number;
}
export const ProductListFilter: React.FC = () => {
const searchParams = useSearchParams();
const createQueryString = (params: FilterParams) => {
const query = new URLSearchParams(searchParams);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
query.set(key, String(value));
} else {
query.delete(key);
}
});
return query.toString();
};
const currentCategory = searchParams.get('category') || 'all';
const currentPage = parseInt(searchParams.get('page') || '1');
return (
<>
<div className=\"filters\">
{['electronics', 'clothing', 'books'].map((cat) => (
<Link
key={cat}
href={`/products?${createQueryString({
category: cat,
page: 1, // フィルタ変更時はページ1に戻す
})}`}
className={currentCategory === cat ? 'active' : ''}
>
{cat}
</Link>
))}
</div>
<div className=\"pagination\">
{currentPage > 1 && (
<Link
href={`/products?${createQueryString({
page: currentPage - 1,
})}`}
>
Previous
</Link>
)}
<Link
href={`/products?${createQueryString({
page: currentPage + 1,
})}`}
>
Next
</Link>
</div>
</>
);
};
3.3 動的ルーティングとリンク
ブログ記事やユーザープロフィールなど、IDベースの動的ページへのリンクは実務の基本です。
// components/BlogPostCard.tsx
'use client';
import Link from 'next/link';
import Image from 'next/image';
interface BlogPost {
id: number;
slug: string;
title: string;
excerpt: string;
thumbnail: string;
author: string;
publishedAt: string;
}
interface BlogPostCardProps {
post: BlogPost;
}
export const BlogPostCard: React.FC<BlogPostCardProps> = ({ post }) => {
return (
<article className=\"post-card\">
<Link href={`/blog/${post.slug}`} className=\"post-link\">
<div className=\"post-image\">
<Image
src={post.thumbnail}
alt={post.title}
width={400}
height={300}
/>
</div>
<div className=\"post-content\">
<h3>{post.title}</h3>
<p>{post.excerpt}</p>
<div className=\"post-meta\">
<span>By {post.author}</span>
<time dateTime={post.publishedAt}>
{new Date(post.publishedAt).toLocaleDateString('ja-JP')}
</time>
</div>
</div>
</Link>
</article>
);
};
// app/blog/page.tsx
import { BlogPostCard } from '@/components/BlogPostCard';
export default async function BlogPage() {
const posts = await fetchBlogPosts();
return (
<div className=\"blog-list\">
{posts.map((post) => (
<BlogPostCard key={post.id} post={post} />
))}
</div>
);
}
3.4 内部リンク・外部リンク判定ラッパー
リンク先がURLスキーム(http/https)か相対パスかで自動的に切り替えるコンポーネントは、実務で頻繁に使用します。
// components/SmartLink.tsx
'use client';
import Link from 'next/link';
import { ReactNode } from 'react';
interface SmartLinkProps {
href: string;
children: ReactNode;
className?: string;
target?: string;
rel?: string;
onClick?: () => void;
}
export const SmartLink: React.FC<SmartLinkProps> = ({
href,
children,
className,
target,
rel,
onClick,
}) => {
// 外部リンクの判定
const isExternal = href.startsWith('http://') || href.startsWith('https://');
if (isExternal) {
return (
<a
href={href}
className={className}
target={target || '_blank'}
rel={rel || 'noopener noreferrer'}
onClick={onClick}
>
{children}
</a>
);
}
return (
<Link href={href} className={className} onClick={onClick}>
{children}
</Link>
);
};
// 使用例
export const Footer: React.FC = () => {
return (
<footer>
<nav>
<SmartLink href=\"/privacy\">Privacy Policy</SmartLink>
<SmartLink href=\"https://twitter.com/ourcompany\">
Follow us on Twitter
</SmartLink>
<SmartLink href=\"/terms\">Terms of Service</SmartLink>
</nav>
</footer>
);
};
3.5 プリフェッチの最適化
パフォーマンスを考慮して、プリフェッチを選別する実装は重要です。
// components/OptimizedLink.tsx
'use client';
import Link from 'next/link';
import { ReactNode } from 'react';
interface OptimizedLinkProps {
href: string;
children: ReactNode;
className?: string;
prefetch?: boolean; // 明示的にプリフェッチを制御
prefetchOnHover?: boolean; // ホバー時にプリフェッチ
}
export const OptimizedLink: React.FC<OptimizedLinkProps> = ({
href,
children,
className,
prefetch = true,
prefetchOnHover = false,
}) => {
// 重いページや低速接続では自動プリフェッチを無効化
const shouldPrefetch = prefetch &&
!prefetchOnHover &&
(navigator.connection as any)?.saveData !== true;
return (
<Link
href={href}
className={className}
prefetch={shouldPrefetch}
onMouseEnter={() => {
if (prefetchOnHover) {
// マウスホバー時にプリフェッチを実行
(window as any).requestIdleCallback?.(() => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = href;
document.head.appendChild(link);
});
}
}}
>
{children}
</Link>
);
};
4. よくある応用パターン
4.1 トランジション効果付きナビゲーション
Next.js 13+のuseTransitionを組み合わせて、スムーズなページ遷移を実装するパターンです。
// components/TransitionLink.tsx
'use client';
import Link from 'next/link';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
interface TransitionLinkProps {
href: string;
children: React.ReactNode;
className?: string;
onTransitionStart?: () => void;
}
export const TransitionLink: React.FC<TransitionLinkProps> = ({
href,
children,
className,
onTransitionStart,
}) => {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
onTransitionStart?.();
startTransition(() => {
router.push(href);
});
};
return (
<Link
href={href}
className={`${className} ${isPending ? 'loading' : ''}`}
onClick={handleClick}
aria-busy={isPending}
>
{children}
</Link>
);
};
4.2 ブレッドクラムの実装
ユーザーの現在位置を示すブレッドクラムナビゲーションも、Linkコンポーネントをうまく活用することで実装できます。
// components/Breadcrumb.tsx
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface BreadcrumbItem {
label: string;
href: string;
}
export const Breadcrumb: React.FC = () => {
const pathname = usePathname();
// パスをセグメントに分割
const segments = pathname
.split('/')
.filter(Boolean)
.map((segment, index, arr) => ({
label: decodeURIComponent(segment)
.replace(/-/g, ' ')
.toUpperCase(),
href: '/' + arr.slice(0, index + 1).join('/'),
}));
return (
<nav aria-label=\"Breadcrumb\" className=\"breadcrumb\">
<ol>
<li>
<Link href=\"/\">Home</Link>
</li>
{segments.map((item, index) => (
<li key={item.href}>
{index === segments.length - 1 ? (
<span aria-current=\"page\">{item.label}</span>
) : (
<Link href={item.href}>{item.label}</Link>
)}
</li>
))}
</ol>
</nav>
);
};
4.3 タブメニューの実装
URLクエリパラメータを活用したタブメニューの実装は、実務で頻出します。
// components/TabNavigation.tsx
'use client';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
interface Tab {
id: string;
label: string;
}
interface TabNavigationProps {
tabs: Tab[];
paramKey?: string;
}
export const TabNavigation: React.FC<TabNavigationProps> = ({
tabs,
paramKey = 'tab',
}) => {
const searchParams = useSearchParams();
const activeTab = searchParams.get(paramKey) || tabs[0]?.id;
const createTabUrl = (tabId: string) => {
const params = new URLSearchParams(searchParams);
params.set(paramKey, tabId);
return `?${params.toString()}`;
};
return (
<div className=\"tabs\">
<div className=\"tab-list\" role=\"tablist\">
{tabs.map((tab) => (
<Link
key={tab.id}
href={createTabUrl(tab.id)}
role=\"tab\"
aria-selected={activeTab === tab.id}
className={`tab ${activeTab === tab.id ? 'active' : ''}`}
>
{tab.label}
</Link>
))}
</div>
</div>
);
};
5. 実務での注意点
5.1 プリフェッチの副作用
自動プリフェッチは便利ですが、バックエンド負荷やAPI制限に影響を与えることがあります。特に課金対象のAPIを使用している場合は注意が必要です。
// 外部APIを使用する場合のプリフェッチ無効化例
<Link href=\"/expensive-api-page\" prefetch={false}>
Click to load
</Link>
5.2 認証トークンの有効期限
認証が必要なページへのリンク遷移時、トークン有効期限切れでログインページにリダイレクトされるケースがあります。以下のように事前にバリデーションすることが重要です。
// middleware.ts(Edge MiddlewareまたはNext.jsミドルウェア)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('authToken')?.value;
const pathname = request.nextUrl.pathname;
// 保護されたパスの場合
if (pathname.startsWith('/dashboard')) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// トークン有効期限チェック
try {
const decoded = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
if (decoded.exp * 1000 < Date.now()) {
return NextResponse.redirect(new URL('/login', request.url));
}
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
};
5.3 SEO対策
内部リンクのSEO価値を活かすため、Linkコンポーネント内のテキストは意味のあるものにしましょう。「こちら」「詳細」といった曖昧なテキストは避けるべきです。
// 避けるべき例
<Link href=\"/products/laptop\">Click here</Link>
// 改善された例
<Link href=\"/products/laptop\">
Latest Gaming Laptops Under $1000
</Link>
5.4 クライアント・サーバーコンポーネント境界
Linkコンポーネントはクライアント側で機能する必要があります。サーバーコンポーネント内で使用する際は、childrenをクライアントコンポーネントに渡すようにしましょう。
// app/products/page.tsx (Server Component)
import Link from 'next/link';
import { ProductCard } from '@/components/ProductCard';
export default async function ProductsPage() {
const products = await fetchProducts();
return (
<div className=\"products\">
{products.map((product) => (
<Link key={product.id} href={`/products/${product.id}`}>
<ProductCard product={product} />
</Link>
))}
</div>
);
}
5.5 パフォーマンス計測
実務では、Linkコンポーネントを使用した遷移のパフォーマンスを計測し、改善することが重要です。
// lib/analytics.ts
export const trackNavigation = (href: string, duration: number) => {
if (typeof window !== 'undefined') {
// Google Analyticsやカスタムトラッキング
(window as any).gtag?.('event', 'page_view', {
page_path: href,
page_load_time: duration,
});
}
};
// components/TrackedLink.tsx
'use client';
import Link from 'next/link';
import { useTransition } from 'react';
import { trackNavigation } from '@/lib/analytics';
export const TrackedLink: React.FC<{
href: string;
children: React.ReactNode;
}> = ({ href, children }) => {
const [isPending, startTransition] = useTransition();
const handleClick = () => {
const startTime = performance.now();
startTransition(() => {
setTimeout(() => {
const duration = performance.now() - startTime;
trackNavigation(href, duration);
}, 0);
});
};
return (
<Link href={href} onClick={handleClick}>
{children}
</Link>
);
};
6. まとめ
Next.js Linkコンポーネントは、単純なナビゲーション機能に見えますが、実務では多くの工夫が必要です。本記事で紹介したパターンは以下の通りです:
- 認証ベースの制御:ユーザー状態に応じた柔軟なリンク管理
- クエリパラメータ管理:フィルタ、ページング、ソートの実装
- 動的ルーティング:スラッグやIDベースのURL構築
- 内外リンク判定:自動的なアンカータグとの切り替え
- プリフェッチ最適化:パフォーマンスとバックエンド負荷のバランス
- トランジション効果:ユーザー体験の向上
これらを適切に組み合わせることで、ユーザーフレンドリーで高速、かつメンテナンスしやすいNext.jsアプリケーションを構築できます。プロジェクトの要件に応じて、カスタムフックやラッパーコンポーネントを作成し、チーム全体で一貫性のあるリンク実装を心がけましょう。

