Next.js 15とReact 19の組み合わせで、Webアプリケーション開発が大きく進化しました。本記事では、実際に動作するEコマースアプリケーションを例に、新機能の活用方法から移行手順まで、現場で使える実践的なノウハウを詳しく解説します。
Next.js 15 × React 19で変わること
革命的な新機能
React 19の主要新機能
- use hook: 非同期リソースの読み込み
- useActionState: フォーム状態管理の簡素化
- useOptimistic: 楽観的アップデートの実装
- useFormStatus: フォーム送信状態の管理
Next.js 15の改善点
- Turbopack Dev: 開発サーバーの高速化(最大10倍)
- AsyncAPI: リクエスト関連APIの非同期化
- 改善されたキャッシュ戦略: より柔軟なキャッシュ制御
最短で課題解決する一冊
この記事の内容と高い親和性が確認できたベストマッチです。早めにチェックしておきましょう。
実践的なEコマースアプリを構築
実際に動作する商品管理システムを通して、新機能を学びましょう。
プロジェクト初期設定
# Next.js 15プロジェクトの作成
npx create-next-app@latest ecommerce-app \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd ecommerce-app
# React 19へのアップグレード
npm install react@rc react-dom@rc
npm install --save-dev @types/react@rc @types/react-dom@rcプロジェクト構成
src/
├── app/
│ ├── api/
│ │ └── products/
│ │ ├── route.ts
│ │ └── [id]/route.ts
│ ├── products/
│ │ ├── page.tsx
│ │ ├── [id]/page.tsx
│ │ └── components/
│ │ ├── ProductForm.tsx
│ │ ├── ProductList.tsx
│ │ └── OptimisticCart.tsx
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── lib/
│ ├── actions.ts
│ ├── db.ts
│ └── types.ts
└── components/
└── ui/TypeScript型定義
// lib/types.ts
export interface Product {
id: string;
name: string;
price: number;
description: string;
stock: number;
createdAt: Date;
updatedAt: Date;
}
export interface CartItem {
productId: string;
quantity: number;
product: Product;
}
export interface User {
id: string;
email: string;
name: string;
}
export interface FormState {
message: string;
errors?: Record<string, string[]>;
success?: boolean;
}
export interface OptimisticAction {
type: 'ADD_TO_CART' | 'REMOVE_FROM_CART' | 'UPDATE_QUANTITY';
payload: any;
id: string;
}さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
React 19の新機能を活用
1. useActionStateでフォーム管理
// lib/actions.ts - Server Actions
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
import { FormState } from './types';
const ProductSchema = z.object({
name: z.string().min(1, 'Product name is required'),
price: z.number().min(0.01, 'Price must be greater than 0'),
description: z.string().min(10, 'Description must be at least 10 characters'),
stock: z.number().int().min(0, 'Stock must be a non-negative integer'),
});
export async function createProduct(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const validatedFields = ProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description'),
stock: Number(formData.get('stock')),
});
if (!validatedFields.success) {
return {
message: 'Validation failed',
errors: validatedFields.error.flatten().fieldErrors,
};
}
const { name, price, description, stock } = validatedFields.data;
try {
// データベースに商品を保存(実際のDB操作に置き換え)
await new Promise(resolve => setTimeout(resolve, 1000)); // API遅延をシミュレート
const product = {
id: Date.now().toString(),
name,
price,
description,
stock,
createdAt: new Date(),
updatedAt: new Date(),
};
console.log('Created product:', product);
revalidatePath('/products');
return {
message: 'Product created successfully!',
success: true,
};
} catch (error) {
return {
message: 'Failed to create product. Please try again.',
success: false,
};
}
}
export async function updateCartItem(
productId: string,
quantity: number
): Promise<{ success: boolean; message: string }> {
await new Promise(resolve => setTimeout(resolve, 500));
return {
success: true,
message: `Cart updated: Product ${productId}, Quantity: ${quantity}`,
};
}2. 商品追加フォームコンポーネント
// app/products/components/ProductForm.tsx
'use client';
import { useActionState } from 'react';
import { createProduct } from '@/lib/actions';
import { FormState } from '@/lib/types';
const initialState: FormState = {
message: '',
};
export default function ProductForm() {
const [state, formAction, isPending] = useActionState(createProduct, initialState);
return (
<div className="max-w-md mx-auto bg-white p-6 rounded-lg shadow-lg">
<h2 className="text-2xl font-bold mb-6 text-gray-800">Add New Product</h2>
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Product Name
</label>
<input
type="text"
id="name"
name="name"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter product name"
/>
{state.errors?.name && (
<p className="text-red-500 text-sm mt-1">{state.errors.name[0]}</p>
)}
</div>
<div>
<label htmlFor="price" className="block text-sm font-medium text-gray-700 mb-1">
Price ($)
</label>
<input
type="number"
id="price"
name="price"
step="0.01"
min="0"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0.00"
/>
{state.errors?.price && (
<p className="text-red-500 text-sm mt-1">{state.errors.price[0]}</p>
)}
</div>
<div>
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
Description
</label>
<textarea
id="description"
name="description"
required
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter product description"
/>
{state.errors?.description && (
<p className="text-red-500 text-sm mt-1">{state.errors.description[0]}</p>
)}
</div>
<div>
<label htmlFor="stock" className="block text-sm font-medium text-gray-700 mb-1">
Stock Quantity
</label>
<input
type="number"
id="stock"
name="stock"
min="0"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="0"
/>
{state.errors?.stock && (
<p className="text-red-500 text-sm mt-1">{state.errors.stock[0]}</p>
)}
</div>
<button
type="submit"
disabled={isPending}
className={`w-full py-2 px-4 rounded-md text-white font-medium ${
isPending
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
}`}
>
{isPending ? 'Creating Product...' : 'Create Product'}
</button>
{state.message && (
<div
className={`p-3 rounded-md ${
state.success
? 'bg-green-100 text-green-700 border border-green-200'
: 'bg-red-100 text-red-700 border border-red-200'
}`}
>
{state.message}
</div>
)}
</form>
</div>
);
}3. useOptimisticでリアルタイムカート
// app/products/components/OptimisticCart.tsx
'use client';
import { useOptimistic, useState, startTransition } from 'react';
import { updateCartItem } from '@/lib/actions';
import { CartItem, Product } from '@/lib/types';
interface OptimisticCartProps {
initialCart: CartItem[];
products: Product[];
}
export default function OptimisticCart({ initialCart, products }: OptimisticCartProps) {
const [optimisticCart, addOptimisticUpdate] = useOptimistic(
initialCart,
(state: CartItem[], { type, productId, quantity }: {
type: 'ADD' | 'UPDATE' | 'REMOVE';
productId: string;
quantity: number;
}) => {
switch (type) {
case 'ADD':
const product = products.find(p => p.id === productId);
if (!product) return state;
const existingItem = state.find(item => item.productId === productId);
if (existingItem) {
return state.map(item =>
item.productId === productId
? { ...item, quantity: item.quantity + quantity }
: item
);
}
return [...state, { productId, quantity, product }];
case 'UPDATE':
return state.map(item =>
item.productId === productId
? { ...item, quantity }
: item
);
case 'REMOVE':
return state.filter(item => item.productId !== productId);
default:
return state;
}
}
);
const [isUpdating, setIsUpdating] = useState<Record<string, boolean>>({});
const handleAddToCart = async (productId: string, quantity: number = 1) => {
setIsUpdating(prev => ({ ...prev, [productId]: true }));
startTransition(() => {
addOptimisticUpdate({ type: 'ADD', productId, quantity });
});
try {
await updateCartItem(productId, quantity);
} catch (error) {
console.error('Failed to add to cart:', error);
} finally {
setIsUpdating(prev => ({ ...prev, [productId]: false }));
}
};
const handleUpdateQuantity = async (productId: string, newQuantity: number) => {
if (newQuantity <= 0) {
handleRemoveItem(productId);
return;
}
setIsUpdating(prev => ({ ...prev, [productId]: true }));
startTransition(() => {
addOptimisticUpdate({ type: 'UPDATE', productId, quantity: newQuantity });
});
try {
await updateCartItem(productId, newQuantity);
} catch (error) {
console.error('Failed to update cart:', error);
} finally {
setIsUpdating(prev => ({ ...prev, [productId]: false }));
}
};
const handleRemoveItem = async (productId: string) => {
setIsUpdating(prev => ({ ...prev, [productId]: true }));
startTransition(() => {
addOptimisticUpdate({ type: 'REMOVE', productId, quantity: 0 });
});
try {
await updateCartItem(productId, 0);
} catch (error) {
console.error('Failed to remove item:', error);
} finally {
setIsUpdating(prev => ({ ...prev, [productId]: false }));
}
};
const totalPrice = optimisticCart.reduce(
(total, item) => total + (item.product.price * item.quantity),
0
);
const totalItems = optimisticCart.reduce((total, item) => total + item.quantity, 0);
return (
<div className="bg-white p-6 rounded-lg shadow-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-gray-800">Shopping Cart</h2>
<div className="text-sm text-gray-600">
{totalItems} item{totalItems !== 1 ? 's' : ''}
</div>
</div>
{optimisticCart.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<p>Your cart is empty</p>
</div>
) : (
<div className="space-y-4">
{optimisticCart.map(item => (
<div
key={item.productId}
className={`flex items-center justify-between p-4 border rounded-lg ${
isUpdating[item.productId] ? 'bg-gray-50 opacity-75' : 'bg-white'
}`}
>
<div className="flex-1">
<h3 className="font-semibold text-gray-800">{item.product.name}</h3>
<p className="text-sm text-gray-600">${item.product.price.toFixed(2)}</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => handleUpdateQuantity(item.productId, item.quantity - 1)}
disabled={isUpdating[item.productId]}
className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
>
-
</button>
<span className="w-12 text-center font-medium">
{item.quantity}
</span>
<button
onClick={() => handleUpdateQuantity(item.productId, item.quantity + 1)}
disabled={isUpdating[item.productId]}
className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-200 hover:bg-gray-300 disabled:opacity-50"
>
+
</button>
<button
onClick={() => handleRemoveItem(item.productId)}
disabled={isUpdating[item.productId]}
className="ml-4 text-red-600 hover:text-red-800 disabled:opacity-50"
>
Remove
</button>
</div>
</div>
))}
<div className="border-t pt-4">
<div className="flex justify-between items-center text-lg font-bold">
<span>Total:</span>
<span>${totalPrice.toFixed(2)}</span>
</div>
</div>
</div>
)}
<div className="mt-6 grid grid-cols-2 gap-4">
{products.slice(0, 4).map(product => (
<div key={product.id} className="border rounded-lg p-4">
<h4 className="font-medium">{product.name}</h4>
<p className="text-sm text-gray-600">${product.price.toFixed(2)}</p>
<button
onClick={() => handleAddToCart(product.id)}
disabled={isUpdating[product.id]}
className={`mt-2 w-full py-1 px-3 rounded text-sm ${
isUpdating[product.id]
? 'bg-gray-300 text-gray-500'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isUpdating[product.id] ? 'Adding...' : 'Add to Cart'}
</button>
</div>
))}
</div>
</div>
);
}4. useフックでデータ読み込み
// app/products/components/ProductDetails.tsx
'use client';
import { use } from 'react';
import { notFound } from 'next/navigation';
import { Product } from '@/lib/types';
async function fetchProduct(id: string): Promise<Product> {
// API遅延をシミュレート
await new Promise(resolve => setTimeout(resolve, 1000));
// 実際のAPIエンドポイントに置き換え
const mockProduct: Product = {
id,
name: `Product ${id}`,
price: Math.floor(Math.random() * 100) + 10,
description: `This is a detailed description for product ${id}. It includes all the important information about the product features and benefits.`,
stock: Math.floor(Math.random() * 50) + 1,
createdAt: new Date(),
updatedAt: new Date(),
};
return mockProduct;
}
interface ProductDetailsProps {
productId: string;
}
export default function ProductDetails({ productId }: ProductDetailsProps) {
// React 19の新しいuseフックを使用
const product = use(fetchProduct(productId));
if (!product) {
notFound();
}
return (
<div className="max-w-4xl mx-auto bg-white rounded-lg shadow-lg overflow-hidden">
<div className="md:flex">
<div className="md:w-1/2 bg-gray-200 h-96 flex items-center justify-center">
<span className="text-gray-500 text-lg">Product Image</span>
</div>
<div className="md:w-1/2 p-8">
<h1 className="text-3xl font-bold text-gray-800 mb-4">{product.name}</h1>
<div className="mb-6">
<span className="text-4xl font-bold text-blue-600">
${product.price.toFixed(2)}
</span>
</div>
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-800 mb-2">Description</h3>
<p className="text-gray-600 leading-relaxed">{product.description}</p>
</div>
<div className="mb-6">
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
product.stock > 10
? 'bg-green-100 text-green-800'
: product.stock > 0
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
</span>
</div>
<button
disabled={product.stock === 0}
className={`w-full py-3 px-6 rounded-lg font-semibold ${
product.stock === 0
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
}`}
>
{product.stock === 0 ? 'Out of Stock' : 'Add to Cart'}
</button>
</div>
</div>
</div>
);
}さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
Next.js 14から15への移行ガイド
自動移行スクリプト
#!/bin/bash
# migrate-nextjs-15.sh - Next.js 15移行スクリプト
echo "🚀 Next.js 15 移行を開始..."
# Step 1: バックアップ
echo "📦 プロジェクトのバックアップを作成..."
cp -r . ../$(basename "$PWD")-backup-$(date +%Y%m%d)
# Step 2: 依存関係の更新
echo "📥 Next.js 15とReact 19にアップデート..."
npm install next@latest react@rc react-dom@rc
npm install --save-dev @types/react@rc @types/react-dom@rc eslint-config-next@latest
# Step 3: 自動コード変換
echo "🔧 自動コード変換を実行..."
npx @next/codemod@canary upgrade latest
# Step 4: 設定ファイルの更新
echo "⚙️ 設定ファイルを確認..."
if [ -f "next.config.js" ]; then
echo "next.config.jsが見つかりました。TypeScript版への変換を推奨します。"
fi
# Step 5: テスト実行
echo "🧪 テストを実行..."
if npm test 2>/dev/null; then
echo "✅ テスト成功"
else
echo "⚠️ テストをスキップ(テストスクリプトが見つかりません)"
fi
# Step 6: ビルドテスト
echo "🏗️ ビルドテスト..."
if npm run build; then
echo "✅ ビルド成功"
else
echo "❌ ビルドエラー。修正が必要です。"
exit 1
fi
echo "🎉 Next.js 15への移行が完了しました!"
echo "📚 変更点を確認してください: https://nextjs.org/docs/app/guides/upgrading/version-15"TypeScript設定の最適化
// next.config.ts - TypeScript対応の設定ファイル
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: false,
},
eslint: {
ignoreDuringBuilds: false,
},
experimental: {
ppr: true, // Partial Prerendering
reactCompiler: true, // React Compiler(実験的)
},
images: {
formats: ['image/webp', 'image/avif'],
},
// 新しいキャッシュ設定
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // インメモリキャッシュを無効化
};
export default nextConfig;さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
パフォーマンス比較テスト
// benchmark/performance-test.ts - パフォーマンステスト
import { performance } from 'perf_hooks';
interface BenchmarkResult {
operation: string;
timeMs: number;
version: string;
}
class PerformanceTester {
private results: BenchmarkResult[] = [];
async testServerComponentsRendering(): Promise<void> {
const start = performance.now();
// Server Components のレンダリング時間を測定
for (let i = 0; i < 100; i++) {
// 模擬的なServer Componentレンダリング処理
await new Promise(resolve => setTimeout(resolve, 1));
}
const end = performance.now();
this.results.push({
operation: 'Server Components Rendering (100 components)',
timeMs: end - start,
version: 'Next.js 15 + React 19'
});
}
async testFormSubmission(): Promise<void> {
const start = performance.now();
// フォーム送信パフォーマンステスト
const formData = new FormData();
formData.append('name', 'Test Product');
formData.append('price', '29.99');
formData.append('description', 'Test description');
formData.append('stock', '10');
// 模擬的なServer Actionの実行
for (let i = 0; i < 50; i++) {
await new Promise(resolve => setTimeout(resolve, 5));
}
const end = performance.now();
this.results.push({
operation: 'Form Submission with Server Actions (50 submissions)',
timeMs: end - start,
version: 'Next.js 15 + React 19'
});
}
async runAllTests(): Promise<void> {
console.log('Starting performance tests...\n');
await this.testServerComponentsRendering();
await this.testFormSubmission();
this.printResults();
}
printResults(): void {
console.log('\n🚀 Next.js 15 + React 19 Performance Results:');
console.log('================================================');
this.results.forEach(result => {
console.log(`${result.operation}:`);
console.log(` Time: ${result.timeMs.toFixed(2)}ms`);
console.log(` Version: ${result.version}\n`);
});
}
}
// テスト実行
const tester = new PerformanceTester();
tester.runAllTests().catch(console.error);
export default PerformanceTester;さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
プロダクション環境への対応
Docker設定
# Dockerfile - Next.js 15対応
FROM node:20-alpine AS base
# Dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
# Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。
まとめ
Next.js 15とReact 19の組み合わせにより、Webアプリケーション開発が劇的に改善されました。
主な改善点
- 開発者体験の向上: useActionStateによる簡潔なフォーム管理
- ユーザー体験の改善: useOptimisticによる即座のフィードバック
- パフォーマンス向上: Turbopack Devと改善されたキャッシュ戦略
- 型安全性の強化: TypeScriptとの完全統合
移行時のポイント
- 段階的な移行: 自動化ツールを活用した安全な移行
- 充分なテスト: 新機能が既存の機能に影響しないことを確認
- パフォーマンス監視: 本番環境でのメトリクス収集
2025年現在、Next.js 15とReact 19の組み合わせは、モダンなWebアプリケーション開発のデファクトスタンダードとなっています。本記事の実装例を参考に、ぜひあなたのプロジェクトでも最新技術を活用してください。
さらに理解を深める参考書
関連記事と相性の良い実践ガイドです。手元に置いて反復しながら進めてみてください。




![Ansible実践ガイド 第4版[基礎編] impress top gearシリーズ](https://m.media-amazon.com/images/I/516W+QJKg1L._SL500_.jpg)