ReactサーバーコンポーネントとReactバージョン19を使った削除・編集ページの書き方(参考情報)

公開日:2025年2月1日


RSCとReact v19の導入

第8章では編集・削除ページ全体を、Reactクライアントコンポーネントを使って開発しました。これは入門書としてコードの見通しのよさを優先させたためですが、これらのページでは「全体をReactサーバーコンポーネントで作り、form部分だけをクライアントコンポーネントにする」という構造にすることも可能です。そして実はこのような、「Reactクライアントコンポーネントの使用を最小限にとどめる」という設計こそ、Next.js AppルータでReactサーバーコンポーネントがデフォルトとなっている理由です。

そのためここでは参考情報として、削除ページを「全体をReactサーバーコンポーネント、form部分だけをクライアントコンポーネント」で作る方法、および編集ページを「全体をReactサーバーコンポーネント、form部分だけをクライアントコンポーネント、プラスReactバージョン19(2024年12月安定版リリース)の新機能」で作る方法を簡単に紹介します。なお、これは参考情報なので、この部分を飛ばして先に進んでも問題ありません。

Reactサーバーコンポーネントを使った削除ページ

削除ページから作業を進めます。最初にすることは、「アイテムデータをひとつだけ取得する」というコードを、他のファイルでも使えるようにすることです。アイテムをひとつ取得するファイルを開き、getSingleItemexportを追加してください。

// 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を作ってください。

rsc-delete-1.jpg

この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で行っているので、それを受け取ることができます。contextpropsに変更してください。

// 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を作ってください。

rsc-delete-2.jpg

ここには次のコードを書きます。

// app/item/delete/[id]/loading.js

const Loading = () => <h2>Loading...</h2>

export default Loading

これで削除ページを、Next.jsの現在の流儀に沿った設計に変更できました。次に手をつける編集ページも、作業はこの削除ページとほとんど同じです。なお、form.jsがクライアントコンポーネントになっている理由ですが、このページで使っているuseAuth.js内にuseEffectというクライアントコンポーネントでしか動かないコードがあるためです。

ReactサーバーコンポーネントとReactバージョン19を使った編集ページ

updateフォルダの[id]フォルダ内に、新しいファイルform.jsを作ってください。

rsc-update-1.jpg

ここに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を作り、そこに下記コードを書いておきましょう。

rsc-update-2.jpg

// 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>タグのsubmitactionに変更し、そして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の新機能を導入できました。