公開日:2025年2月1日
第8章では編集・削除ページ全体を、Reactクライアントコンポーネントを使って開発しました。これは入門書としてコードの見通しのよさを優先させたためですが、これらのページでは「全体をReactサーバーコンポーネントで作り、form部分だけをクライアントコンポーネントにする」という構造にすることも可能です。そして実はこのような、「Reactクライアントコンポーネントの使用を最小限にとどめる」という設計こそ、Next.js AppルータでReactサーバーコンポーネントがデフォルトとなっている理由です。
そのためここでは参考情報として、削除ページを「全体をReactサーバーコンポーネント、form部分だけをクライアントコンポーネント」で作る方法、および編集ページを「全体をReactサーバーコンポーネント、form部分だけをクライアントコンポーネント、プラスReactバージョン19(2024年12月安定版リリース)の新機能」で作る方法を簡単に紹介します。なお、これは参考情報なので、この部分を飛ばして先に進んでも問題ありません。
削除ページから作業を進めます。最初にすることは、「アイテムデータをひとつだけ取得する」というコードを、他のファイルでも使えるようにすることです。アイテムをひとつ取得するファイルを開き、getSingleItemにexportを追加してください。
// app/item/readsingle/[id]/page.js
import Image from "next/image"
import Link from "next/link"
// ⬇追加
export const getSingleItem = async(id) => {
const response = await fetch(`http://localhost:3000/api/item/readsingle/${id}`)
const jsonData = await response.json()
const singleItem = jsonData.singleItem
return singleItem
}
const ReadSingleItem = async(context) => {
...
下準備ができました。次は削除ページです。deleteフォルダの[id]フォルダに、新しいファイルform.jsを作ってください。
このform.jsに、page.jsに書かれている削除ページのコードをすべて移動させます。
// app/item/delete/[id]/form.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) => {
const [title, setTitle] = useState("")
const [price, setPrice] = useState("")
const [image, setImage] = useState("")
....
<p>{description}</p>
<button>削除</button>
</form>
</div>
)
}else{
return <h1>権限がありません</h1>
}
}
export default DeleteItem
コードを移動させた後のpage.jsには、次のコードを書きます。ここでしていることは、「データをひとつ取得して、contextと一緒に<Form>タグに渡す」ということだけですが、ここにはuse clientの記述がないため、今この削除ページ全体はReactサーバーコンポーネントになっていることがわかります。
// app/item/delete/[id]/page.js
import Form from "./form"
import { getSingleItem } from "../../readsingle/[id]/page"
const DeleteItem = async(context) => {
const params = await context.params
const singleItem = await getSingleItem(params.id)
return (
<div>
<h1 className="page-title">アイテム削除</h1>
<Form params={params} singleItem={singleItem}/>
</div>
)
}
export default DeleteItem
次はform.jsを開き、次のコードを削除してください。
// app/item/delete/[id]/form.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) => {
// ⬇削除
const [title, setTitle] = useState("")
const [price, setPrice] = useState("")
const [image, setImage] = useState("")
const [description, setDescription] = useState("")
const [email, setEmail] = useState("")
// ⬆削除
const router = useRouter()
const loginUserEmail = useAuth()
// ⬇削除
useEffect(() => {
const getSingleItem = async() => {
const params = await context.params
const response = await fetch(`http://localhost:3000/api/item/readsingle/${params.id}`)
const jsonData = await response.json()
const singleItem = jsonData.singleItem
setTitle(singleItem.title)
setPrice(singleItem.price)
setImage(singleItem.image)
setDescription(singleItem.description)
setEmail(singleItem.email)
}
getSingleItem()
}, [context])
// ⬆削除
const handleSubmit = async(e) => {
...
データ取得を行っていたuseEffectと、そのデータを一時保管していたuseState部分を消してしまいましたが、データ取得はすでにpage.jsで行っているので、それを受け取ることができます。contextをpropsに変更してください。
// app/item/delete/[id]/form.js
"use client"
import { useRouter } from "next/navigation"
import Image from "next/image"
import useAuth from "../../../utils/useAuth"
const DeleteItem = (props) => { // 変更
const router = useRouter()
const loginUserEmail = useAuth()
const handleSubmit = async(e) => {
...
このprops内にはpage.jsから渡されたparamsと、アイテムデータがひとつ入ったsingleItemがあるので、これを使います。
// app/item/delete/[id]/form.js
...
const handleSubmit = async(e) => {
e.preventDefault()
const params = await context.params // 削除
try{ // ⬇変更
const response = await fetch(`http://localhost:3000/api/item/delete/${props.params.id}`, {
method: "DELETE",
headers: {
...
}
// ⬇変更
if(loginUserEmail === props.singleItem.email){
return (
<div>
<h1 className="page-title">アイテム削除</h1> // 削除
<form onSubmit={handleSubmit}>
<h2>{props.singleItem.title}</h2> // 変更
<Image src={props.singleItem.image} width={750} height={500} alt="item-image" priority/> // 変更
<h3>¥{props.singleItem.price}</h3> // 変更
<p>{props.singleItem.description}</p> // 変更
<button>削除</button>
</form>
</div>
)
}else{
return <h1>権限がありません</h1>
}
}
export default DeleteItem
さて、Next.jsのAppルータのReactサーバーコンポーネントでは、loading.jsという名前のファイルを同じフォルダ内に作ると、それがローディングとして使われます。loading.jsを作ってください。
ここには次のコードを書きます。
// app/item/delete/[id]/loading.js
const Loading = () => <h2>Loading...</h2>
export default Loading
これで削除ページを、Next.jsの現在の流儀に沿った設計に変更できました。次に手をつける編集ページも、作業はこの削除ページとほとんど同じです。なお、form.jsがクライアントコンポーネントになっている理由ですが、このページで使っているuseAuth.js内にuseEffectというクライアントコンポーネントでしか動かないコードがあるためです。
updateフォルダの[id]フォルダ内に、新しいファイルform.jsを作ってください。
ここにpage.jsのコードをすべて移し、そして移したあとのpage.jsには次のコードを書きます。
// app/item/update/[id]/page.js
import Form from "./form"
import { getSingleItem } from "../../readsingle/[id]/page"
const UpdateItem = async(context) => {
const params = await context.params
const singleItem = await getSingleItem(params.id)
return (
<div>
<h1 className="page-title">アイテム編集</h1>
<Form params={params} singleItem={singleItem}/>
</div>
)
}
export default UpdateItem
form.jsのコードには次の修正を加えます。
// app/item/update/[id]/form.js
"use client"
import { useState, useEffect } from "react" // 削除
import { useRouter } from "next/navigation"
import useAuth from "../../../utils/useAuth"
const UpdateItem = (context) => { // 「context」を「props」に変更
// ⬇削除
const [title, setTitle] = useState("")
const [price, setPrice] = useState("")
const [image, setImage] = useState("")
const [description, setDescription] = useState("")
const [email, setEmail] = useState("")
// ⬆削除
const router = useRouter()
const loginUserEmail = useAuth()
// ⬇削除
useEffect(() => {
const getSingleItem = async() => {
const params = await context.params
const response = await fetch(`http://localhost:3000/api/item/readsingle/${params.id}`)
const jsonData = await response.json()
const singleItem = jsonData.singleItem
setTitle(singleItem.title)
setPrice(singleItem.price)
setImage(singleItem.image)
setDescription(singleItem.description)
setEmail(singleItem.email)
}
getSingleItem()
}, [context])
// ⬆削除
const handleSubmit = async(e) => {
e.preventDefault()
const params = await context.params // 削除
try{ // ⬇変更
const response = await fetch(`http://localhost:3000/api/item/update/${props.params.id}`, {
method: "PUT",
headers: {
...
}
// ⬇変更
if(loginUserEmail === props.singleItem.email){
return (
<div>
<h1 className="page-title">アイテム編集</h1> // 削除
<form onSubmit={handleSubmit}>
// ⬇「value」を「defaultValue」に変え、「props.singleItem.」を追加
<input defaultValue={props.singleItem.title} onChange={(e) => setTitle(e.target.value)} type="text" name="title" placeholder="アイテム名" required/>
<input defaultValue={props.singleItem.price} onChange={(e) => setPrice(e.target.value)} type="text" name="price" placeholder="価格" required/>
<input defaultValue={props.singleItem.image} onChange={(e) => setImage(e.target.value)} type="text" name="image" placeholder="画像" required/>
<textarea defaultValue={props.singleItem.description} onChange={(e) => setDescription(e.target.value)} name="description" rows={15} placeholder="商品説明" required></textarea>
// ⬆「value」を「defaultValue」に変え、「props.singleItem.」を追加
<button>編集</button>
</form>
</div>
)
}else{
return <h1>権限がありません</h1>
}
}
export default UpdateItem
loading.jsを作り、そこに下記コードを書いておきましょう。
// app/item/update/[id]/loading.js
const Loading = () => <h2>Loading...</h2>
export default Loading
ここまでは削除ページと同じでした。
ここからはReactバージョン19で追加された、Actionsという機能を導入してコードを書き換えます。これは<form>タグにaction属性を使ってfunctionを渡せる機能で、入力されたデータはformDataで取得できます。handleSubmit部分を次のように書き換えてください。
// app/item/update/[id]/form.js
const handleSubmit = async(formData) => { // 変更
e.preventDefault() // 削除
try{
const response = await fetch(`http://localhost:3000/api/item/update/${props.params.id}`, {
method: "PUT",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify({
// ⬇変更
title: formData.get("title"),
price: formData.get("price"),
image: formData.get("image"),
description: formData.get("description"),
// ⬆変更
email: loginUserEmail
})
})
const jsonData = await response.json()
...
formData.get()のカッコ内に書かれている文字は、各<input>および<textarea>タグのname属性です。
最後に<form>タグのsubmitをactionに変更し、そしてonChange部分はすべて削除してください。次のようになります。
// app/item/update/[id]/form.js
<form action={handleSubmit}>
<input defaultValue={props.singleItem.title} type="text" name="title" placeholder="アイテム名" required/>
<input defaultValue={props.singleItem.price} type="text" name="price" placeholder="価格" required/>
<input defaultValue={props.singleItem.image} type="text" name="image" placeholder="画像" required/>
<textarea defaultValue={props.singleItem.description} name="description" rows={15} placeholder="商品説明" required></textarea>
<button>編集</button>
</form>
これで削除ページと編集ページにReactサーバーコンポーネントと、Reactバージョン19の新機能を導入できました。