Next.jsの新しいバージョン(15)の変更点について

Next.jsの新しいバージョンの変更点

本書は執筆時点での最新バージョンのNext.js(バージョン14)をベースに解説を進めています。しかし本書発売後の2024年10月下旬、Next.jsの新しいバージョン(バージョン15)がリリースされたため、本書の説明の通りに下記コマンドでNext.jsをインストールすると、自動でバージョン15がインストールされます。

npx create-next-app next-market

Next.jsバージョン15では大きな変更がいくつも加えられているため、本書の通りに進めるとエラーの出る箇所があります。以下、変更点や対応策を紹介します。


変更点 1(インストール時の質問)

現在(2024年11月)、上記Next.jsインストールコマンド実行時の質問に、Turbopackに関するものが追加されています。

? Would you like to use Turbopack for next dev? … No / Yes

TurbopackとはNext.jsの開発元Vercel社が開発を主導しているバンドラー(開発を高速化するツール)です。本書ではTurbopackは使っていないので、「No」を選択してください。


変更点 2(contextの使い方)

本書ではバックエンドとフロントエンド両方において、contextというコードを複数の箇所で使っています。Next.jsバージョン15ではcontextの使い方が変更されたため、本書の通りに進めるとエラーが発生します。次の2つの方法のどちらかで対応をしてください。

変更点 2(contextの使い方)の対応策A:バーションを準拠させる(推奨)

本書はNext.jsバージョン14を元に書かれているので、バージョンを準拠させることでスムーズに読み進めていくことができます。

バージョン15ではReactのバージョンも変更されているため、React関係のパッケージのバージョンも準拠させます。Next.jsインストール完了後にVS Codeを開き、次のコマンドを実行してください。

npm install next@14.1.4 react@18 react-dom@18

完了後にpackage.jsonを開き、dependenciesに次のように書かれていれば、バージョンが本書と同じになっています。

// package.json

...

"dependencies": {
    "next": "^14.1.4",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
}
...

変更点 2(contextの使い方)の対応策B:コードを変更する

バージョンを14に変えずに15を使う場合は、これから説明する変更をコードに加えてください。

本書ではcontextをURL取得のために使っています。最初に登場するのは、Chapter 3(第3章)のMongoDBからアイテムを1つ読み取る機能を開発する時です。

本書で使っているバージョン14では、contextの内部へはcontext.params.idのようにダイレクトにアクセスできました。しかしバージョン15ではawaitを使う必要があるため、次のように1行余分に書く必要があります。

// バージョン14(本書)
console.log(context.params.id)

// バージョン15
const params = await context.params
console.log(params.id)

バージョン15を使って本書を進める方は、contextの利用時には上の書き方で対応してください。

以下、本書を終えた時点での完成見本コードを用いて、バージョン15を使った場合に変更すべき箇所を紹介します。まずはバックエンド関係のファイルからです。

// app/api/item/readSingle/[id]/route.js(アイテムをひとつ読み取る機能)

import { NextResponse } from "next/server"
import connectDB from "../../../../utils/database"
import { ItemModel } from "../../../../utils/schemaModels"

export async function GET(request, context){
    try{
        await connectDB()
        const params = await context.params                       // 追加
        const singleItem = await ItemModel.findById(params.id)    // 変更
        return NextResponse.json({message: "アイテム読み取り成功(シングル)", singleItem: singleItem})
    }catch{
        ...
// app/api/item/update/[id]/route.js(アイテムの編集機能)

import { NextResponse } from "next/server"
import connectDB from "../../../../utils/database"
import { ItemModel } from "../../../../utils/schemaModels" 

export async function PUT(request, context){
    const reqBody = await request.json() 
    try{
        await connectDB()
        const params = await context.params                         // 追加
        const singleItem = await ItemModel.findById(params.id)      // 変更
        if(singleItem.email === reqBody.email){
            await ItemModel.updateOne({_id: params.id}, reqBody)   // 変更
            return NextResponse.json({message: "アイテム編集成功"})
        }else{
            return NextResponse.json({message: "他の人が作成したアイテムです"})
        }
    }catch{
        ...
// app/api/item/delete/[id]/route.js(アイテムの削除機能)

import { NextResponse } from "next/server"
import connectDB from "../../../../utils/database"
import { ItemModel } from "../../../../utils/schemaModels"

export async function DELETE(request, context){
    const reqBody = await request.json()
    try{
        await connectDB()
        const params = await context.params                       // 追加
        const singleItem = await ItemModel.findById(params.id)    // 変更
        if(singleItem.email === reqBody.email){
            await ItemModel.deleteOne({_id: params.id})           // 変更
            return NextResponse.json({message: "アイテム削除成功"})
        }else{
            return NextResponse.json({message: "他の人が作成したアイテムです"})
        }
    }catch{
        ...

次はフロントエンド関係のファイルです。

// app/item/readSingle/[id]/page.js(アイテムをひとつ読み取るページ)

import Image from "next/image"  
import Link from "next/link" 

const getSingleItem = async(id) => {
    
    ...

}  

const ReadSingleItem = async(context) => {
    const params = await context.params                // 追加
    const singleItem = await getSingleItem(params.id)  // 変更
    return (
        <div className="grid-container-si">
            ...
// app/item/update/[id]/page.js(アイテム編集ページ)

"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation" 
import useAuth from "../../../utils/useAuth"

const UpdateItem = (context) => {
    
    ...

    useEffect(() => {
        const getSingleItem = async() => {           // 「id」を削除
            const params = await context.params      // 追加                                    // ↓変更
            const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/item/readsingle/${params.id}`, {cache: "no-store"})
            const jsonData = await response.json() 
            const singleItem = jsonData.singleItem
            setTitle(singleItem.title)
            setPrice(singleItem.price)
            setImage(singleItem.image)
            setDescription(singleItem.description)
            setEmail(singleItem.email) 
            setLoading(true) 
        }  
        getSingleItem()                         // 「context.params.id」を削除
    }, [context]) 

    const handleSubmit = async(e) => {
        e.preventDefault() 
        const params = await context.params        // 追加 
        try{                                                                               // ↓変更
            const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/item/update/${params.id}`, {
                method: "PUT",
                headers: { 
                    "Accept": "application/json", 
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${localStorage.getItem("token")}`
                },
                ...

// app/item/delete/[id]/page.js(アイテム削除ページ)


"use client"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation" 
import Image from "next/image"   
import useAuth from "../../../utils/useAuth"

const DeleteItem = (context) => {
    
    ...

    useEffect(() => {
        const getSingleItem = async() => {             // 「id」を削除
            const params = await context.params       // 追加                                  // ↓変更
            const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/item/readsingle/${params.id}`, {cache: "no-store"})
            const jsonData = await response.json() 
            const singleItem = jsonData.singleItem
            setTitle(singleItem.title)
            setPrice(singleItem.price)
            setImage(singleItem.image)
            setDescription(singleItem.description)
            setEmail(singleItem.email) 
            setLoading(true)  
        }  
        getSingleItem()                          // 「context.params.id」を削除
    }, [context]) 

    const handleSubmit = async(e) => {
        e.preventDefault() 
        const params = await context.params         // 追加 
        try{                                                                               // ↓変更
            const response = await fetch(`${process.env.NEXT_PUBLIC_URL}/api/item/delete/${params.id}`, {
                method: "DELETE",
                headers: { 
                    "Accept": "application/json", 
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${localStorage.getItem("token")}`
                },
                ...