本書は執筆時点での最新バージョンのNext.js(バージョン14)をベースに解説を進めています。しかし本書発売後の2024年10月下旬、Next.jsの新しいバージョン(バージョン15)がリリースされたため、本書の説明の通りに下記コマンドでNext.jsをインストールすると、自動でバージョン15がインストールされます。
npx create-next-app next-market
Next.jsバージョン15では大きな変更がいくつも加えられているため、本書の通りに進めるとエラーの出る箇所があります。以下、変更点や対応策を紹介します。
現在(2024年11月)、上記Next.jsインストールコマンド実行時の質問に、Turbopackに関するものが追加されています。
? Would you like to use Turbopack for next dev? … No / Yes
TurbopackとはNext.jsの開発元Vercel社が開発を主導しているバンドラー(開発を高速化するツール)です。本書ではTurbopackは使っていないので、「No」を選択してください。
本書ではバックエンドとフロントエンド両方において、context
というコードを複数の箇所で使っています。Next.jsバージョン15ではcontext
の使い方が変更されたため、本書の通りに進めるとエラーが発生します。次の2つの方法のどちらかで対応をしてください。
本書は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"
}
...
バージョンを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")}`
},
...