Next.js

Reactベースのフルスタックフレームワーク。SSR、SSG、ISRを標準サポートし、高性能なWebアプリケーション開発を可能にする。企業採用率No.1の地位を確立。

JavaScriptTypeScriptReactフレームワークSSGSSRISRフルスタックVercel

GitHub概要

vercel/next.js

The React Framework

ホームページ:https://nextjs.org
スター133,753
ウォッチ1,484
フォーク29,050
作成日:2016年10月5日
言語:JavaScript
ライセンス:MIT License

トピックス

blogbrowsercompilercomponentshybridnextjsnodereactserver-renderingssgstaticstatic-site-generatoruniversalvercel

スター履歴

vercel/next.js Star History
データ取得日時: 2025/8/13 01:43

フレームワーク

Next.js

概要

Next.jsは、ReactベースのフルスタックWebアプリケーション開発フレームワークです。

詳細

Next.js(ネクストJS)は、Vercel(旧Zeit)が開発したReactベースのフルスタックWebアプリケーション開発フレームワークです。2016年にリリースされて以来、Reactアプリケーション開発のデファクトスタンダードとして広く採用されています。静的サイト生成(SSG)、サーバーサイドレンダリング(SSR)、増分静的再生成(ISR)など複数のレンダリング手法をサポートし、パフォーマンス、SEO、開発者体験を大幅に向上させます。App RouterとPages Routerの2つのルーティングシステムを提供し、React Server Componentsなど最新のReact機能にも対応しています。自動コード分割、画像最適化、TypeScript統合、API Routes、ミドルウェアなど豊富な機能を内蔵し、設定なしで本格的なWebアプリケーションを構築できます。現在、Nike、Netflix、Notion、TikTokなど世界規模のサービスで採用されており、Webフロントエンド開発の最前線を牽引しています。

メリット・デメリット

メリット

  • ゼロ設定: Webpack、Babel、TypeScript等を自動設定
  • 複数レンダリング手法: SSG、SSR、ISRを柔軟に選択可能
  • 優秀なパフォーマンス: 自動最適化によるCore Web Vitals向上
  • 優れたSEO: サーバーサイドレンダリングによる検索エンジン対応
  • 豊富な内蔵機能: 画像最適化、フォント最適化、コード分割等
  • 優れた開発体験: Hot Reload、TypeScript統合、ESLint統合
  • 強力なエコシステム: Vercel連携とコミュニティサポート
  • 最新React対応: Server Components、Suspense等をサポート

デメリット

  • 学習コスト: App Router、Server Components等の新概念
  • バージョン間の変更: メジャーアップデートでの破壊的変更
  • ホスティング制約: フル機能利用にはNode.js環境が必要
  • オーバーエンジニアリング: 小規模プロジェクトには過剰な場合も
  • デバッグの複雑さ: サーバー・クライアント間のデバッグ
  • ベンダーロックイン: Vercel固有機能への依存リスク

主要リンク

書き方の例

Hello World(Pages Router)

// pages/index.js
export default function Home() {
  return (
    <div>
      <h1>Hello, Next.js!</h1>
      <p>Pages Routerを使用したシンプルなページです。</p>
    </div>
  );
}

Hello World(App Router)

// app/page.js
export default function Home() {
  return (
    <div>
      <h1>Hello, Next.js!</h1>
      <p>App Routerを使用したシンプルなページです。</p>
    </div>
  );
}

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

Static Site Generation(SSG)

Pages Router

// pages/blog/[slug].js
export default function BlogPost({ post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

// ビルド時にページを生成
export async function getStaticProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await res.json();

  return {
    props: { post },
    // 60秒後に再生成(ISR)
    revalidate: 60,
  };
}

// 動的ルートのパスを定義
export async function getStaticPaths() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  const paths = posts.map((post) => ({
    params: { slug: post.slug },
  }));

  return {
    paths,
    fallback: 'blocking', // 新しいパスをリクエスト時に生成
  };
}

App Router

// app/blog/[slug]/page.js
export const revalidate = 60; // ISRの設定

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) =>
    res.json()
  );

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }) {
  const { slug } = await params;
  
  // データフェッチ(SSG)
  const post = await fetch(`https://api.example.com/posts/${slug}`, {
    cache: 'force-cache', // デフォルト(省略可能)
  }).then((res) => res.json());

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

Server-Side Rendering(SSR)

Pages Router

// pages/dashboard.js
export default function Dashboard({ user, posts }) {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ、{user.name}さん</p>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

// リクエストごとにサーバーで実行
export async function getServerSideProps(context) {
  const { req } = context;
  
  // セッションからユーザー情報を取得
  const user = await getUserFromSession(req);
  
  // ユーザー固有のデータを取得
  const posts = await fetch(`https://api.example.com/users/${user.id}/posts`)
    .then(res => res.json());

  return {
    props: { user, posts },
  };
}

App Router

// app/dashboard/page.js
import { cookies, headers } from 'next/headers';

export default async function Dashboard() {
  // 動的APIの使用でSSRに切り替わる
  const cookieStore = await cookies();
  const headersList = await headers();
  
  // リクエストごとにデータフェッチ
  const user = await fetch('https://api.example.com/user', {
    cache: 'no-store', // SSRの設定
    headers: {
      'Authorization': `Bearer ${cookieStore.get('token')?.value}`,
    },
  }).then(res => res.json());

  const posts = await fetch(`https://api.example.com/users/${user.id}/posts`, {
    cache: 'no-store',
  }).then(res => res.json());

  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ようこそ、{user.name}さん</p>
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

API Routes

Pages Router

// pages/api/users/[id].js
export default async function handler(req, res) {
  const { id } = req.query;

  if (req.method === 'GET') {
    try {
      const user = await getUserById(id);
      res.status(200).json(user);
    } catch (error) {
      res.status(404).json({ error: 'User not found' });
    }
  } else if (req.method === 'POST') {
    try {
      const updatedUser = await updateUser(id, req.body);
      res.status(200).json(updatedUser);
    } catch (error) {
      res.status(500).json({ error: 'Update failed' });
    }
  } else {
    res.setHeader('Allow', ['GET', 'POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

App Router(Route Handlers)

// app/api/users/[id]/route.js
import { NextResponse } from 'next/server';

export async function GET(request, { params }) {
  const { id } = await params;
  
  try {
    const user = await getUserById(id);
    return NextResponse.json(user);
  } catch (error) {
    return NextResponse.json(
      { error: 'User not found' },
      { status: 404 }
    );
  }
}

export async function POST(request, { params }) {
  const { id } = await params;
  const body = await request.json();
  
  try {
    const updatedUser = await updateUser(id, body);
    return NextResponse.json(updatedUser);
  } catch (error) {
    return NextResponse.json(
      { error: 'Update failed' },
      { status: 500 }
    );
  }
}

フォーム処理(Server Actions)

// app/contact/page.js
import { redirect } from 'next/navigation';

async function submitContact(formData) {
  'use server'; // Server Action
  
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  // バリデーション
  if (!name || !email || !message) {
    throw new Error('必須項目を入力してください');
  }
  
  // データベースに保存
  await saveContact({ name, email, message });
  
  // リダイレクト
  redirect('/contact/success');
}

export default function ContactForm() {
  return (
    <form action={submitContact}>
      <div>
        <label htmlFor="name">名前:</label>
        <input type="text" id="name" name="name" required />
      </div>
      <div>
        <label htmlFor="email">メール:</label>
        <input type="email" id="email" name="email" required />
      </div>
      <div>
        <label htmlFor="message">メッセージ:</label>
        <textarea id="message" name="message" rows={4} required />
      </div>
      <button type="submit">送信</button>
    </form>
  );
}

データ再検証とキャッシュ管理

// app/admin/posts/page.js
import { revalidatePath, revalidateTag } from 'next/cache';

async function createPost(formData) {
  'use server';
  
  const title = formData.get('title');
  const content = formData.get('content');
  
  // 新しい投稿を作成
  await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ title, content }),
    next: { tags: ['posts'] }, // タグ付けしてキャッシュ
  });
  
  // 特定のパスのキャッシュを無効化
  revalidatePath('/blog');
  
  // タグ付きキャッシュを無効化
  revalidateTag('posts');
}

export default async function AdminPosts() {
  // タグ付きでデータフェッチ
  const posts = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] },
  }).then(res => res.json());

  return (
    <div>
      <h1>投稿管理</h1>
      <form action={createPost}>
        <input name="title" placeholder="タイトル" required />
        <textarea name="content" placeholder="内容" required />
        <button type="submit">投稿作成</button>
      </form>
      
      <ul>
        {posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

ミドルウェア

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  // 認証チェック
  const token = request.cookies.get('auth-token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  // レスポンスヘッダーの追加
  const response = NextResponse.next();
  response.headers.set('x-middleware', 'true');
  
  return response;
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};