【App Router入門】新機能を使ったブログアプリを図解する

Featured image of the post

はじめに

Next.jsのApp Routerを使ってブログアプリ(最小限の構成)を作った🔰

App Routerの新しい機能を使ったり、CRUD操作をしたりと勉強になったので備忘録を残す😊

💡
App Routerに焦点を当てて最小限のプログラムを作ったため、バリデーションやエラーハンドリングなど本来必要な処理は入れていません。

作ったもの

ブログの投稿を作成、編集、削除ができるアプリを作った。

投稿の一覧

投稿を一覧表示する。

Image in a image block

投稿の作成

「タイトル」「本文」を入力して投稿を作成する。

Image in a image block

投稿の編集

「タイトル」「本文」を編集する。

Image in a image block

投稿の削除

削除ボタンをクリックして削除する。

Image in a image block

フォルダ構成
Image in a image block

使用したApp Router周りの機能
  • App Routerのルーティング
  • サーバーコンポーネント、クライアントコンポーネント
  • Server Actions

App Routerって何?

そもそもApp Routerって何?はこちらで解説している。

準備

使用する手法・技術の紹介

事前にインストールしておくもの

  • Node.js
  • npm
  • Next.js
  • Prisma
  • サーバー(何でもOK)

    例:SQLite、ローカルPCにインストールしたMySQL、ネットワーク上のサーバーなど

今からインストールするもの

  • Prisma

Prismaをインストールする

prismaをインストールする。

npm install prisma

Prisma Clientをインストールする。

(プログラムからデータベースにアクセスするために必要)

npm install @prisma/client

Prismaを初期化する
npx prisma init

これで「prismaフォルダ」と「.envファイル」ができる。

Image in a image block

データベースを新規作成

✅これからPrismaでデータベース操作をするので、事前に手動でデータベースそのものを用意する。

今回はMySQLのデータベースを作ってみる。

(PCにインストール済みのMAMPにMySQLが備わっていたのでそれを使う)

💡
以下のデータベース作成はあくまで一例。好きなデータベースでOK。

【手順1】phpMyAdminにアクセス

MAMPを起動しphpMyAdminを開く。

Image in a image block

【手順2】データベースを新規作成

今回は「nextjs-blog」という名前のデータベースを作成する。

Image in a image block

prisma/schema.prismaの設定

デフォルトは以下のようになっている。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

使用するデータベースに合わせてproviderを変更する。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql" // ✅変更
  url      = env("DATABASE_URL")
}

.envの設定

デフォルトは以下のようになっている。

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

自分のデータベースの情報に書き換える。

// ✅変更
DATABASE_URL="mysql://id:pass@localhost:port番号/DB名?schema=public"

例:MAMPの場合はデフォルトだと以下のようになる。

DATABASE_URL="mysql://root:root@localhost:3306/nextjs-blog?schema=public"

MAMPの場合のポート番号の確認方法
Image in a image block
Image in a image block

テーブルを新規作成

以下の手順でテーブルが作れる。

【手順1】prisma/schema.prisma にテーブル定義を書く

以下のようにmodelを書けばOK。

今回はPostsテーブルを作成する。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

// ✅追記
model Posts {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  createdAt DateTime @default(now())
  updatedAt DateTime?
}

【手順2】マイグレートする

コマンドを実行するだけでOK。

npx prisma migrate dev --name init

成功した画面

Image in a image block

✅実行結果

phpMyAdminを見ると、データベース「nextjs-blog」にテーブル「Posts」ができている。

Image in a image block

【手順1】Server Actionsで使う関数を準備

やりたいこと

今回はApp Routerの新機能「Server Actions」を使いたい。

Server Actionstとは?

クライアントからバックエンドの関数を呼び出せる機能✨

具体的にはフォーム送信時などにバックエンドの関数を呼び出せる。

💡
これによりAPIを作らずにバックエンドの処理を呼び出せる😊

具体的には以下の処理をするバックエンドの関数をフォーム送信時に呼び出したい。

  • 投稿の作成
  • 投稿の削除
  • 投稿の更新

💡
これから上記3つの関数を作る!

【補足】投稿の取得はServer Actionsでなくてもいい

(疑問)

「投稿の取得」も作成、削除、編集と同様CRUD操作の一部。

投稿の取得もServer Actionsに含めた方がいい?💭

(答え)

投稿の取得はクライアントから呼び出す関数ではないのでServer Actionsに含めなくていい。

→サーバー側の初期レンダリングで呼び出す。

💡
サーバー側から呼ばれる処理はServer Actionsではなく、直接コンポーネント内に書いてOK。

【手順1-1】データベース接続用モジュールの作成

データベース接続するためのファイルを作っておく。

✅PrismaClientを生成することでデータベース操作ができる。

→今後はこのファイルをインポートするだけでデータベース操作ができる。

src/app/lib/prisma.js

/**
 * Prisma Clientをインポートします。
 */
import { PrismaClient } from "@prisma/client";

let prisma;

/**
 * 環境に応じてPrismaClientのインスタンスを作成します。
 * 本番環境では新しいインスタンスを作成し、それ以外の環境ではグローバルなインスタンスを再利用します。
 */
if (process.env.NODE_ENV === "production") {
    prisma = new PrismaClient();
} else {
    if (!global.prisma) {
        global.prisma = new PrismaClient();
    }
    prisma = global.prisma;
}

export default prisma;

【手順1-2】関数を作る

1つのファイルに作成、削除、編集の関数をまとめて定義しておく。

src/app/lib/posts.js

// ✅Server Actions
"use server";

// ✅データベース接続用モジュール
import prisma from "@/app/lib/prisma";

/**
 * 新規投稿をデータベースに作成します。
 * @async
 * @function addPost
 * @param {Object} data - 新規投稿のデータ
 */
export const addPost = async (data) => {
  // 入力値を取得
  const title = data.get("title");
  const body = data.get("body");

  // データベースに1レコード作成
  await prisma.posts.create({
    data: {
      title,
      body,
    },
  });
};

/**
 * 指定されたIDの投稿を削除します。
 *
 * @param {FormData} data - フォームデータ。'id'フィールドには削除する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 削除操作が完了すると解決するPromise。
 */
export const deletePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");

  // データベースから1レコード削除
  await prisma.posts.delete({
    where: {
      id: parseInt(id),
    },
  });
};


/**
 * 指定されたIDの投稿を更新します。
 * 
 * @param {FormData} data - フォームデータ。'id'フィールドには更新する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 更新操作が完了すると解決するPromise。
 */
export const updatePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");
  const title = data.get("title");
  const body = data.get("body");

  // データベースのレコードを更新
  await prisma.posts.update({
    where: {
      id: parseInt(id),
    },
    data: {
      title: title,
      body: body,
    },
  });
};

【解説】✅Server Actions
// ✅Server Actions
"use server";

Server Actionsで使う関数は「私はクライアントから呼び出せる関数だよ〜」な状態にしておく必要がある。

→ファイルの先頭に"use server";をつけるだけでOK!

【解説】✅データベース接続用モジュール
// ✅データベース接続用モジュール
import prisma from "@/app/lib/prisma";

【手順1-1】で作成したファイルをインポートしている。

これだけでデータベース操作が可能になる。

【解説】投稿の作成、削除、編集
/**
 * 新規投稿をデータベースに作成します。
 * @async
 * @function addPost
 * @param {Object} data - 新規投稿のデータ
 */
export const addPost = async (data) => {
  // 入力値を取得
  const title = data.get("title");
  const body = data.get("body");

  // データベースに1レコード作成
  await prisma.posts.create({
    data: {
      title,
      body,
    },
  });
};

/**
 * 指定されたIDの投稿を削除します。
 *
 * @param {FormData} data - フォームデータ。'id'フィールドには削除する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 削除操作が完了すると解決するPromise。
 */
export const deletePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");

  // データベースから1レコード削除
  await prisma.posts.delete({
    where: {
      id: parseInt(id),
    },
  });
};


/**
 * 指定されたIDの投稿を更新します。
 * 
 * @param {FormData} data - フォームデータ。'id'フィールドには更新する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 更新操作が完了すると解決するPromise。
 */
export const updatePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");
  const title = data.get("title");
  const body = data.get("body");

  // データベースのレコードを更新
  await prisma.posts.update({
    where: {
      id: parseInt(id),
    },
    data: {
      title: title,
      body: body,
    },
  });
};

Prismaで単純なデータベース操作をしているだけ。

Prismaの話なので詳細は割愛する。

【手順2】全体のレイアウト

layout.jsで全ページ共通のレイアウトを定義する。

layout.jsとは?

共通のレイアウトやメタデータなどを定義するファイル。

App Router専用。

【手順2-1】適当なレイアウトを作成

src/app/layout.js

import { Inter } from 'next/font/google'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'ブログアプリ',
  description: 'ブログアプリのデモです。',
}

export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body className={inter.className}>{children}</body>
    </html>
  )
}

💡
今回はレイアウトはこだわらないのでほぼデフォルトのまま。

メタデータだけ書き換えた。

好みに合わせて自由に編集してOK✨

【手順3】投稿一覧ページ

「投稿一覧」と「新規作成ボタン」を表示するページ。

完成イメージ

Image in a image block

【手順3-1】ページを作る

✅URL「/」でアクセスできる投稿一覧ページを作る。

src/app/page.jsx

import Link from 'next/link';
import prisma from "@/app/lib/prisma";
// ✅投稿データ一覧のコンポーネント
import PostList from "./(components)/PostList";

// ✅このページをSSRにする
export const dynamic = 'force-dynamic'

/**
 * 投稿のリストと新規投稿作成へのリンク
 * @async
 * @function Page
 * @returns {JSX.Element} 投稿一覧と新規作成リンクを含むReactコンポーネント
 */
const Page = async () => {
  // ✅投稿データのリスト
  const posts = await prisma.posts.findMany();

  return (
    <div className="m-8">
      <h1 className="text-2xl font-bold mb-4">投稿一覧</h1>
      <PostList posts={posts} />
      <Link href="/create" className="inline-block bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">新規作成</Link>
    </div>
  );
};

export default Page;

【解説】✅投稿データ一覧のコンポーネント
// ✅投稿データ一覧のコンポーネント
import PostList from "./(components)/PostList";

以下の部分を表示するコンポーネント。

【手順3-2】で作る。

Image in a image block

【補足】コンポーネントは必ず分ける

コンポーネントを分けないとサーバーコンポーネント、クライアントコンポーネントの使い分けができない。

(問題点)

  • サーバーコンポーネント、クライアントコンポーネントはそれぞれできないことがある。
    • 例えば…
    • サーバーコンポーネントではuseStateが使えない。
    • クライアントコンポーネントでは直接Prismaを使ったデータベース操作はできない。
  • もし1つのファイルにまとめて書くとサーバーコンポーネント、クライアントコンポーネントどちらかの処理しか書けない💦

【解説】✅このページをSSRにする
// ✅このページをSSRにする
export const dynamic = 'force-dynamic'

これがないとSSGになってしまう。

(問題点)

SSGのままだと投稿を作成や編集をしても一覧データが更新されない💦

具体的にはこの部分がずっと古いまま。

Image in a image block

(解決)

SSRにすることで常に最新のデータが取得できる✨

ちなみに「SSRにする他の方法」や「ISRで解決する方法」もある。

📎
SSRやISRに切り替わる条件はこちらで解説している。

📄 【完全版】Next.jsのSSG、SSR、ISR、CSRを図とコードでスッキリ理解する

【解説】✅投稿データのリスト
// ✅投稿データのリスト
const posts = await prisma.posts.findMany();

以下の部分のデータを取得する。

Image in a image block

(ここで取得したデータは【手順3-2】で作るPostListコンポーネントに渡す)

【手順3-2】投稿一覧を表示するコンポーネントを作る

✅投稿一覧を表示するPostListコンポーネントを作る。

具体的には以下の部分。

Image in a image block

src/app/(components)/PostList.jsx

// ✅クライアントコンポーネント
"use client";
import { useState } from "react";
// ✅投稿コンポーネント
import Post from "./Post";
// ✅投稿削除関数
import { deletePost } from "@/app/lib/posts";

/**
 * 投稿のリストを表示
 * @function PostList
 * @param {Object} props - プロパティオブジェクト
 * @param {Array} props.posts - 投稿データの配列
 * @returns {JSX.Element} 投稿リストを含むReactコンポーネント
 */
const PostList = (props) => {
  // 投稿データのリスト
  const [posts, setPosts] = useState(props.posts);

  /**
   * ✅投稿データを削除
   * @function handleDelete
   * @param {Object} data - 削除する投稿データ
   */
  const handleDelete = (data) => {
    deletePost(data); // データベースから削除
    const id = parseInt( data.get('id') );  // 数値に変換
    setPosts(posts.filter((post) => post.id !== id)); // 画面から削除
  };

  return (
    <table className="mt-8 w-full table-auto">
      <tbody>
        {posts.map((post) => (
          <Post
            key={post.id}
            post={post}
            handleDelete={handleDelete}
          />
        ))}
      </tbody>
    </table>
  );
};

export default PostList;

【解説】✅クライアントコンポーネント
// ✅クライアントコンポーネント
"use client";

useStateを使うためにクライアントコンポーネントにした。

→useStateで投稿一覧の状態を管理して、投稿が減ると即時反映したい。

イメージ

Image in a image block

【他の方法】revalidatePath関数を使う

useStateで投稿一覧を状態管理しなくても、削除後にrevalidatePath関数を実行することで最新データを取得しなおすこともできる。(ISR)

📎
revalidatePath関数でISRにする方法はこちらで解説している。

📄 【完全版】Next.jsのSSG、SSR、ISR、CSRを図とコードでスッキリ理解する

この方法だとPostListコンポーネントをクライアントコンポーネントではなくサーバーコンポーネントのままにしておける。

【解説】✅投稿コンポーネント
// ✅投稿コンポーネント
import Post from "./Post";

投稿1件を表示するコンポーネント。

【手順3-3】で作る。

Image in a image block

【解説】✅投稿削除関数
// ✅投稿削除関数
import { deletePost } from "@/app/lib/posts";

投稿を削除(テーブルから1レコード削除)する関数。

deletePost関数は以下のファイルで定義済み。

【再掲】src/app/lib/posts.js

/**
 * 指定されたIDの投稿を削除します。
 *
 * @param {FormData} data - フォームデータ。'id'フィールドには削除する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 削除操作が完了すると解決するPromise。
 */
export const deletePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");

  // データベースから1レコード削除
  await prisma.posts.delete({
    where: {
      id: parseInt(id),
    },
  });
};

【解説】✅投稿データを削除
/**
 * ✅投稿データを削除
 * @function handleDelete
 * @param {Object} data - 削除する投稿データ
 */
const handleDelete = (data) => {
  deletePost(data); // データベースから削除
  const id = parseInt( data.get('id') );  // 数値に変換
  setPosts(posts.filter((post) => post.id !== id)); // 画面から削除
};

投稿の削除(テーブルから1レコード削除)を呼び出し、画面からも削除する関数。

【手順3-3】投稿1つを表示するコンポーネントを作る

✅投稿1つを表示するPostコンポーネントを作る。

具体的には以下の部分。

Image in a image block

src/app/(components)/Post.jsx

// ✅クライアントコンポーネント
"use client";
import Link from 'next/link'

/**
 * ✅単一の投稿データを表示
 * @function Post
 * @param {Object} props - プロパティオブジェクト
 * @param {Object} props.post - 投稿データ
 * @param {Function} props.handleDelete - 投稿削除のハンドラ関数
 * @returns {JSX.Element} 投稿データを表示するReactコンポーネント
 */
const Post = ({post, handleDelete}) => {
  return (
    <tr key={post.id} className="border-t border-gray-200">
      <td className="px-4 py-2">{post.id}</td>
      <td className="px-4 py-2">{post.title}</td>
      <td className="px-4 py-2">{post.body}</td>
      <td className="px-4 py-2 flex">
				{/* 編集ボタン */}
        <Link href={`edit/${post.id}`} className="text-white bg-blue-500 hover:bg-blue-700 px-2 py-1 rounded">編集</Link>

				{/* 削除ボタン */}
				<form action={handleDelete}>
          <input type="hidden" name="id" value={post.id} />
          <button
            className="text-white bg-red-500 hover:bg-red-700 px-2 py-1 rounded ml-2"
          >
            削除
          </button>
        </form>
      </td>
    </tr>
  );
};

export default Post;

【解説】✅クライアントコンポーネント
// ✅クライアントコンポーネント
"use client";

親コンポーネント(PostList)がクライアントコンポーネントのため、Postもクライアントコンポーネントにした。

(クライアントコンポーネントからサーバーコンポーネントはインポートできないため)

【解説】✅単一の投稿データを表示
/**
 * ✅単一の投稿データを表示
 * @function Post
 * @param {Object} props - プロパティオブジェクト
 * @param {Object} props.post - 投稿データ
 * @param {Function} props.handleDelete - 投稿削除のハンドラ関数
 * @returns {JSX.Element} 投稿データを表示するReactコンポーネント
 */
const Post = ({post, handleDelete}) => {
  return (
    <tr key={post.id} className="border-t border-gray-200">
      <td className="px-4 py-2">{post.id}</td>
      <td className="px-4 py-2">{post.title}</td>
      <td className="px-4 py-2">{post.body}</td>
      <td className="px-4 py-2 flex">
				{/* 編集ボタン */}
        <Link href={`edit/${post.id}`} className="text-white bg-blue-500 hover:bg-blue-700 px-2 py-1 rounded">編集</Link>

				{/* 削除ボタン */}
				<form action={handleDelete}>
          <input type="hidden" name="id" value={post.id} />
          <button
            className="text-white bg-red-500 hover:bg-red-700 px-2 py-1 rounded ml-2"
          >
            削除
          </button>
        </form>
      </td>
    </tr>
  );
};

投稿1件を表示しているだけ。

Image in a image block
Image in a image block

具体的には以下を表示している。

  • id、タイトル、本文
    <td className="px-4 py-2">{post.id}</td>
    <td className="px-4 py-2">{post.title}</td>
    <td className="px-4 py-2">{post.body}</td>

  • 編集ボタン
    {/* 編集ボタン */}
    <Link href={`edit/${post.id}`} className="text-white bg-blue-500 hover:bg-blue-700 px-2 py-1 rounded">編集</Link>
    • ただのリンク。
    • リンク先はidを使って動的に決めている。
    • リンク先の編集ページは【手順5】で作る。

  • 削除ボタン
    {/* 削除ボタン */}
    <form action={handleDelete}>
      <input type="hidden" name="id" value={post.id} />
      <button
        className="text-white bg-red-500 hover:bg-red-700 px-2 py-1 rounded ml-2"
      >
        削除
      </button>
    </form>
    • Server Actionsを使って、削除ボタンクリック時にhandleDelete関数を呼び出す。
      Image in a image block
    • Server Actionsを使うにはformタグが必須。
    • handleDelete関数(【手順3-2】で作った親コンポーネントの関数)が呼ばれると、データベースと画面から投稿が削除される。
    • handleDelete関数にidを渡したいため、非表示の<input>タグを入れている。

【手順4】投稿作成ページ

「タイトル」「本文」を入力して投稿を作成するページ。

完成イメージ

Image in a image block

【手順4-1】ページを作る

✅URL「/create」でアクセスできる投稿作成ページを作る。

src/app/create/page.jsx

import { addPost } from "@/app/lib/posts";

/**
 * 新規投稿を作成するためのフォームを表示します。
 * @async
 * @function Page
 * @returns {JSX.Element} 新規投稿作成フォームを含むReactコンポーネント
 */
const Page = async () => {

  // ✅投稿の内容を入力するフォーム
  return (
    <div className="m-10">
      <h1 className="text-xl font-bold">投稿作成</h1>
      <form className="flex mt-8 flex-col" action={addPost}>
        <label htmlFor="title">タイトル:</label>
        <input
          type="text"
          name="title"
          className="border mx-4 p-1 w-full text-black"
        />
        <label htmlFor="body" className="mt-4">
          本文:
        </label>
        <textarea
          name="body"
          rows="4"
          className="border mx-4 p-1 w-full text-black"
        />
        <button
          type="submit"
          className="bg-blue-600 px-2 py-1 rounded-lg text-sm text-white mx-4 mt-4"
        >
          作成
        </button>
      </form>
    </div>
  );
};

export default Page;

【解説】✅投稿の内容を入力するフォーム
  // ✅投稿の内容を入力するフォーム
  return (
    <div className="m-10">
      <h1 className="text-xl font-bold">投稿作成</h1>
      <form className="flex mt-8 flex-col" action={addPost}>
				省略
      </form>
    </div>
  );

Server Actionsを使って、作成ボタンクリック時にaddPost関数を呼び出す。

Image in a image block

addPost関数は以下のファイルで定義済み。

【再掲】src/app/lib/posts.js

/**
 * 新規投稿をデータベースに作成します。
 * @async
 * @function addPost
 * @param {Object} data - 新規投稿のデータ
 */
export const addPost = async (data) => {
  // 入力値を取得
  const title = data.get("title");
  const body = data.get("body");

  // データベースに1レコード作成
  await prisma.posts.create({
    data: {
      title,
      body,
    },
  });
};

【手順5】投稿編集ページ

「タイトル」「本文」を編集するページ。

完成イメージ

Image in a image block

【手順5-1】ページを作る

URL「/edit/〇〇」でアクセスできる投稿編集ページを作る。

(〇〇には編集したい投稿のidが入る)

src/app/edit/[id]/page.jsx

[id]は動的なパス(URLの〇〇の部分)を表す。

import prisma from "@/app/lib/prisma";
import { updatePost } from "@/app/lib/posts";

/**
 * 指定されたIDの投稿を編集するためのフォームを表示します。
 * @async
 * @function Page
 * @param {Object} params - パラメータオブジェクト
 * @param {string} params.id - 編集する投稿のID
 * @returns {JSX.Element} 投稿編集フォームを含むReactコンポーネント
 */
const Page = async ({params}) => {
  const id = params.id;
  // 投稿データを取得
  const post = await prisma.posts.findUnique({
    where: {
      id: parseInt(id),
    },
  });

	// ✅投稿の内容を編集するフォーム
  return (
    <div className="m-8">
      <h1 className="text-2xl font-bold mb-4">投稿編集</h1>
      <p>ID: {id}</p>
      <form action={updatePost}>
          <input type="hidden" name="id" value={post.id} />
          <input name="title" type="text" defaultValue={post.title} className="w-full px-3 py-2 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300 dark:bg-gray-700 dark:text-white dark:placeholder-gray-500 dark:border-gray-600 dark:focus:ring-gray-900 dark:focus:border-gray-500" />
          <textarea name="body" defaultValue={post.body} className="w-full px-3 py-2 mt-4 placeholder-gray-300 border border-gray-300 rounded-md focus:outline-none focus:ring focus:ring-indigo-100 focus:border-indigo-300 dark:bg-gray-700 dark:text-white dark:placeholder-gray-500 dark:border-gray-600 dark:focus:ring-gray-900 dark:focus:border-gray-500" />
          <button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-700" >更新</button>
      </form>
    </div>
  );
};

export default Page;

【解説】✅投稿の内容を編集するフォーム
// ✅投稿の内容を編集するフォーム
return (
  <div className="m-8">
    <h1 className="text-2xl font-bold mb-4">投稿編集</h1>
    <p>ID: {id}</p>
    <form action={updatePost}>
      省略
    </form>
  </div>
);

Server Actionsを使って、更新ボタンクリック時にupdatePost関数を呼び出す。

Image in a image block

updatePost関数は以下のファイルで定義済み。

【再掲】src/app/lib/posts.js

/**
 * 指定されたIDの投稿を更新します。
 * 
 * @param {FormData} data - フォームデータ。'id'フィールドには更新する投稿のIDが含まれている必要があります。
 * @returns {Promise<void>} 更新操作が完了すると解決するPromise。
 */
export const updatePost = async (data) => {
  // 入力値を取得
  const id = data.get("id");
  const title = data.get("title");
  const body = data.get("body");

  // データベースのレコードを更新
  await prisma.posts.update({
    where: {
      id: parseInt(id),
    },
    data: {
      title: title,
      body: body,
    },
  });
};

【補足】投稿一覧ページのようにexport const dynamic = 'force-dynamic'をつけてSSRにしなくていいの?

(やりたいこと)

編集ページに表示される「タイトル」「本文」は常に最新のデータが必要なのでSSRにしたい。

Image in a image block

(疑問)

export const dynamic = 'force-dynamic'をつけてSSRにする必要はないの?💦

(結論)

編集ページのように動的なパスを含む(URLに〇〇を含む)場合は自動でSSRになるためexport const dynamic = 'force-dynamic'をつける必要はない

まとめ

投稿一覧ページ
Image in a image block
投稿一覧ページ
ファイルパス src/app/page.jsx
URL /
ページの役割 ・全投稿を表示
・投稿の削除
使用コンポーネント ・PostList(投稿一覧)
・Post(単一の投稿)
SSG/SSR/ISR ・SSR
Server Actions ・投稿削除で使用
サーバーコンポーネント/クライアントコンポーネント ・一部クライアントコンポーネント

投稿作成ページ
Image in a image block
投稿作成ページ
ファイルパス src/app/create/page.jsx
URL /create
ページの役割 ・投稿を新規作成
使用コンポーネント
SSG/SSR/ISR ・SSG
Server Actions ・投稿作成で使用
サーバーコンポーネント/クライアントコンポーネント ・サーバーコンポーネント

投稿編集ページ
Image in a image block
投稿編集ページ
ファイルパス src/app/edit/[id]/page.jsx
URL /edit/〇〇
ページの役割 ・投稿を編集
使用コンポーネント
SSG/SSR/ISR ・SSR
Server Actions ・投稿編集で使用
サーバーコンポーネント/クライアントコンポーネント ・サーバーコンポーネント

所感
  • Server Actionsを使うと簡単にフォーム送信時にバックエンドの処理が呼び出せた。
  • コンポーネントを分けてサーバーコンポーネントとクライアントコンポーネントを分離することが重要。
    • useStateなどはクライアントコンポーネントにする。
    • Prismaのデータベース操作などはサーバーコンポーネントにする。
  • ページを作るときはSSG/SSR/ISRを意識する必要がある。
    • 投稿一覧ページはSSGだと一覧データが更新されない。SSRなどにする必要がある。

参考サイト