Next.js Linkコンポーネントの業務実装パターン|実務で使える最適化テクニック

React / Next.js

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アプリケーションを構築できます。プロジェクトの要件に応じて、カスタムフックやラッパーコンポーネントを作成し、チーム全体で一貫性のあるリンク実装を心がけましょう。

タイトルとURLをコピーしました