Closed16

【挑戦】Next.js 15 でチャットアプリを作ってみた

DaiNakaDaiNaka

はじめに

こんにちは、DaiNakaと申します。
普段はLaravelというPHPのフレームワークでウェブサービスを構築していますが、この度、Next.jsに入門しようと思うので、作成の過程を一緒に楽しんでいただけたら幸いです😀

自己紹介

このスクラップを見た方の参考になるように、私の簡単な自己紹介をしたいと思います。
本業はヘルプデスクのため、デベロッパーとしては未経験となります。
2021年頃からLaravelを独学で始めて、いろんなサービスを個人開発したりしています。
この2025年2月頃より、Next.jsの学習を開始しました。

コード(GitHub)

https://github.com/DaiNaka1207/chat-app_nextjs

公開ページ

https://chat-app-nextjs-one.vercel.app/

開発環境

WSL2のUbuntu内で開発をしています

  • node v22.12.0
  • npm 10.9.0
  • pnpm 10.4.1
DaiNakaDaiNaka

作りたいもの

今回はチャットアプリを作っていきたいと思います。
ざっくりなイメージとしては、下記のようなことが学習できればと考えています。

  • データベースの操作
  • フォームからの送信
  • クッキーの操作

機能

今回のチャットアプリでは、主に次の機能を作っていきたいと思います。

  • チャットメッセージを表示する画面
  • メッセージを送信するフォーム
  • ユーザー情報を保存するためのクッキー操作
DaiNakaDaiNaka

プロジェクト作成

プロンプトにてnpx create-next-app@latestを実行して、下記の通り進めていきます。
プロジェクトを作成したらcd chat-appを実行してプロジェクトに入って開発を進めていきます。

What is your project named? chat-app
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to use Turbopack for `next dev`?  Yes
Would you like to customize the import alias (`@/*` by default)? Yes
What import alias would you like configured? @/*
package.json
{
  "name": "chat-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "next": "15.1.7"
  },
  "devDependencies": {
    "typescript": "^5",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "eslint": "^9",
    "eslint-config-next": "15.1.7",
    "@eslint/eslintrc": "^3"
  }
}
DaiNakaDaiNaka

レイアウト修正

ページ全体の色合いを設定するため<body>の背景色を設定しています。また、言語設定も変更しています。

/app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
-   <html lang="en">
+   <html lang="ja">
      <body
-       className={`${geistSans.variable} ${geistMono.variable} antialiased`}
+       className={`${geistSans.variable} ${geistMono.variable} antialiased bg-blue-100`}
      >
        {children}
      </body>
    </html>
  );
}

環境設定

ウェブサービスの名前を環境設定ファイルに設定してページで読み込みます。ルートフォルダ配下に.envを作成して環境変数を書いていきます。

/.env
APP_NAME="Chat-app"

初期ページ

Next.jsの初期ページを消して、下記の通りチャットアプリのような画面に書き換えます。上からタイトル、メッセージエリア、フォームエリアとなります。

/app/page.tsx
export default function Home() {
  const appName = process.env.APP_NAME;
  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">

      {/* Title */}
      <p className="text-2xl font-bold">
        {appName}
      </p>

      {/* Message area */}
      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, ipsam.</p>
      </div>

      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure magni tempore, ad voluptas explicabo exercitationem!</p>
      </div>

      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem ipsum dolor sit amet.</p>
      </div>

      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, autem.</p>
      </div>

      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus accusamus sed cum et sunt, omnis iste doloribus dolore modi. Neque!</p>
      </div>

      {/* Form area */}
      <hr className="mt-5"/>

      <form className="mt-5 flex items-center">
        <div className="flex items-center">
          <label>Name</label>
          <input type="text" className="bg-gray-200 ml-2 p-1 w-24" autoFocus />
        </div>
        
        <div className="ml-5 flex-1 flex items-center">
          <label>Message</label>
          <input type="text" className="bg-gray-200 ml-2 p-1 flex-1" />
        </div>

        <div>
          <button type="button" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
            送信
          </button>
        </div>
      </form>

    </div>
  );
}
DaiNakaDaiNaka

繰り返し部分のリファクタ

メッセージエリアのデータは、現状は直接HTMLを繰り返し記述しています。

/app/page.tsx
      {/* Message area */}
      <div className="mt-5">
        <div className="flex gap-2 text-xs">
          <p>2025-2-21 23:30</p>
          <p>@user</p>
        </div>
        <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, ipsam.</p>
      </div>



このままでは、メンテナンスが必要になると、HTMLを直接修正する必要があり、メンテナンス性が悪い状態です。これをデータ化して渡すことで、データを増やしたり減らしたり、または更新する際のメンテナンス性が向上するので、早速実装していきたいと思います。

参考ページ

と、その前に書き方について調べていきたいと思います。参考にしたのは、下記ページとなります。
https://nextjs.org/docs/app/building-your-application/data-fetching/fetching

上記のページ内では、下記のコードでdataにブログのポストデータを取得して、postsにポストデータをJSON形式に変換しています。

  const data = await fetch('https://api.vercel.app/blog')
  const posts = await data.json()

その後、下記のコードで繰り返しを記述しています。
{変数.map((繰り返し内で使うデータ名) => (HTMLコード))}

      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}

実装

/app/page.tsx
export default function Home() {
  const appName = process.env.APP_NAME;

+ // MessageData
+ const ITEMS = [
+   {
+     id: "1",
+     date: "2025-2-21 23:30",
+     user: "Taro",
+     message: "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure magni tempore, ad voluptas explicabo exercitationem!",
+   },
+   {
+     id: "2",
+     date: "2025-2-21 23:32",
+     user: "Jiro",
+     message: "Lorem ipsum dolor sit amet.",
+   },
+   {
+     id: "3",
+     date: "2025-2-21 23:33",
+     user: "Taro",
+     message: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, autem.",
+   },
+   {
+     id: "4",
+     date: "2025-2-21 23:36",
+     user: "Jiro",
+     message: "Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, ipsam.",
+   },
+   {
+     id: "5",
+     date: "2025-2-21 23:38",
+     user: "Saburo",
+     message: "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus accusamus sed cum et sunt, omnis iste doloribus dolore modi. Neque!",
+   }
+ ];

  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">

      {/* Title */}
      <p className="text-2xl font-bold">
        {appName}
      </p>

      {/* Message area */}
-     <div className="mt-5">
-       <div className="flex gap-2 text-xs">
-         <p>2025-2-21 23:30</p>
-         <p>@user</p>
-       </div>
-       <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, ipsam.</p>
-     </div>
+     {ITEMS.map((item) => {
+       return (
+         <div key={item.id} className="mt-5">
+           <div className="flex gap-2 text-xs">
+             <p>{item.date}</p>
+             <p>@{item.user}</p>
+           </div>
+           <p>{item.message}</p>
+         </div>
+       );
+     })}

-     <div className="mt-5">
-       <div className="flex gap-2 text-xs">
-         <p>2025-2-21 23:30</p>
-         <p>@user</p>
-       </div>
-       <p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure magni tempore, ad voluptas explicabo exercitationem!</p>
-     </div>
-
-       <div className="mt-5">
-       <div className="flex gap-2 text-xs">
-         <p>2025-2-21 23:30</p>
-         <p>@user</p>
-       </div>
-       <p>Lorem ipsum dolor sit amet.</p>
-     </div>
-
-     <div className="mt-5">
-       <div className="flex gap-2 text-xs">
-         <p>2025-2-21 23:30</p>
-         <p>@user</p>
-       </div>
-       <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, autem.</p>
-     </div>
-
-     <div className="mt-5">
-       <div className="flex gap-2 text-xs">
-         <p>2025-2-21 23:30</p>
-         <p>@user</p>
-       </div>
-       <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus accusamus sed cum et sunt, omnis iste doloribus dolore modi. Neque!</p>
-     </div>

      {/* Form area */}
      <hr className="mt-5"/>

      <form className="mt-5 flex items-center">
        <div className="flex items-center">
          <label>Name</label>
          <input type="text" className="bg-gray-200 ml-2 p-1 w-24" autoFocus />
        </div>
        
        <div className="ml-5 flex-1 flex items-center">
          <label>Message</label>
          <input type="text" className="bg-gray-200 ml-2 p-1 flex-1" />
        </div>

        <div>
          <button type="button" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
            送信
          </button>
        </div>
      </form>

    </div>
  );
}
DaiNakaDaiNaka

これまでの成果

Message Dataで定義した内容が反映していると思います。

DaiNakaDaiNaka

データベースの接続

ここでは、メッセージデータをデータベースに登録することができるように接続していきたいと思います。

参考ページ

Next.jsのチュートリアルを参考にしました。ここでは、Vercelを使ってデプロイしているため、Vercelの機能でデータベースを用意しています。
https://nextjs.org/learn/dashboard-app/setting-up-your-database

テーブル作成

データベースはneonを選択しています。テーブルの作成は下記のSQLをご参考ください。

CREATE TABLE messages (
    id SERIAL PRIMARY KEY,
    date TIMESTAMP DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Tokyo'),
    "user" VARCHAR(255) NOT NULL,
    message TEXT NOT NULL
);

接続方法

VercelのダッシュボードからStorage<作成したデータベース名>Getting Startedの順に選択すると、下の方にCreate a form with a Server Actionとしてフォームの書き方がありますので、参考にします。

環境設定ファイルに下記の情報を追加します。

# Recommended for most uses
DATABASE_URL=**********

# For uses requiring a connection without pgbouncer
DATABASE_URL_UNPOOLED=**********

# Parameters for constructing your own connection string
PGHOST=**********
PGHOST_UNPOOLED=**********
PGUSER=**********
PGDATABASE=**********
PGPASSWORD=**********

# Parameters for Vercel Postgres Templates
POSTGRES_URL=**********
POSTGRES_URL_NON_POOLING=**********
POSTGRES_USER=**********
POSTGRES_HOST=**********
POSTGRES_PASSWORD=**********
POSTGRES_DATABASE=**********
POSTGRES_URL_NO_SSL=**********
POSTGRES_PRISMA_URL=**********
/app/page.tsx
+import { neon } from '@neondatabase/serverless';

export default function Home() {
  const appName = process.env.APP_NAME;

  // MessageData
  const ITEMS = [
    {
      id: "1",
      date: "2025-2-21 23:30",
      user: "Taro",
      message: "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure magni tempore, ad voluptas explicabo exercitationem!",
    },
    {
      id: "2",
      date: "2025-2-21 23:32",
      user: "Jiro",
      message: "Lorem ipsum dolor sit amet.",
    },
    {
      id: "3",
      date: "2025-2-21 23:33",
      user: "Taro",
      message: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Aspernatur, autem.",
    },
    {
      key: "4",
      date: "2025-2-21 23:36",
      user: "Jiro",
      message: "Lorem ipsum dolor, sit amet consectetur adipisicing elit. In, ipsam.",
    },
    {
      id: "5",
      date: "2025-2-21 23:38",
      user: "Saburo",
      message: "Lorem ipsum dolor sit amet consectetur, adipisicing elit. Voluptatibus accusamus sed cum et sunt, omnis iste doloribus dolore modi. Neque!",
    }
  ];

+ // データベースへの接続
+ async function create(formData: FormData) {
+   'use server';
+
+   // Connect to the Neon database
+   const sql = neon(`${process.env.DATABASE_URL}`);
+   const user = formData.get('name');
+   const message = formData.get('message');
+   // Insert the comment from the form into the Postgres database
+   await sql('INSERT INTO messages ("user", message) VALUES ($1, $2)', [user, message]);
+ }

  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">

      {/* Title */}
      <p className="text-2xl font-bold">
        {appName}
      </p>

      {/* Message area */}
      {ITEMS.map((item) => {
        return (
          <div key={item.id} className="mt-5">
            <div className="flex gap-2 text-xs">
              <p>{item.date}</p>
              <p>@{item.user}</p>
            </div>
            <p>{item.message}</p>
          </div>
        );
      })}

      {/* Form area */}
      <hr className="mt-5"/>

-     <form className="mt-5 flex items-center">
+     <form className="mt-5 flex items-center" action={create}>
        <div className="flex items-center">
          <label>Name</label>
-         <input type="text" className="bg-gray-200 ml-2 p-1 w-24" autoFocus />
+         <input type="text" name="name" className="bg-gray-200 ml-2 p-1 w-24" autoFocus required />
        </div>
        
        <div className="ml-5 flex-1 flex items-center">
          <label>Message</label>
-         <input type="text" className="bg-gray-200 ml-2 p-1 flex-1" />
+         <input type="text" name="message" className="bg-gray-200 ml-2 p-1 flex-1" required/>
        </div>

        <div>
-         <button type="button" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
+         <button type="submit" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
            送信
          </button>
        </div>
      </form>

    </div>
  );
}
DaiNakaDaiNaka

これまでの成果

表示上は何も変化はありませんが、フォームからメッセージを送信してみてください。

そして、データベースのテーブルを確認すると、レコードが登録されていることが分かるかと思います。

DaiNakaDaiNaka

データベースからのデータ取得

これまでのコードでデータベースにデータを登録するところまでは書けたので、データベースからデータを取得するところを進めていきたいと思います。

ChatGPT先生との対話

参考になりそうなページを探してみましたが、残念ながら見つからず、ChatGPTに聞いてみることにしました。/app/page.tsxのコードを丸々渡して、データベースからのデータの取得について追記してもらいました。

学習の仕方

聞くだけでは身に着けることができないので、コードを一つ一つ分解してみました。

追加されたコード
  // データベースからメッセージを取得
  async function getMessages() {
    const sql = neon(`${process.env.DATABASE_URL}`);
    const rows = await sql('SELECT id, date, "user", message FROM messages');
    return rows;
  }
  • const sqlでデータベースに接続
  • const rowsでSQL文でデータを取得

といったところで解釈しています。実際の処理については今後もっと理解を深める必要がありそうですが、console.log(rows)を実行するとデータベースに登録されている情報が取得できていたので、いったん良しとします。

画面に反映

データベースからのデータの取得ができたので、実際に画面に表示していきたいと思います。ITEMSに入れていきます。

/app/page.tsx
-const ITEMS = [
-   {
-     id: "1",
-     date: "2025-2-21 23:30",
-     user: "Taro",
-     message: "Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure magni tempore, ad voluptas explicabo exercitationem!",
-   },
-   {
-     id: "2",
-     date: "2025-2-21 23:32",
-     user: "Jiro",
-     message: "Lorem ipsum dolor sit amet.",
-   },
-];
+  const ITEMS = await getMessages();

このまま画面を表示しようとすると、エラーになりました。

どうも日付の形式ではダメだと怒られている様子なので、形式を変更。

/app/page.tsx
      {/* Message area */}
      {ITEMS.map((item) => {
        return (
          <div key={item.id} className="mt-5">
            <div className="flex gap-2 text-xs">
-             <p>{item.date}</p>
+             <p>{new Date(item.date).toLocaleString()}</p>
              <p>@{item.user}</p>
            </div>
            <p>{item.message}</p>
          </div>
        );
      })}

画面更新

フォームを送信した後、データベースへの登録が行われるが、画面更新をしないことから反映されていないように見える状況となっている。そこで、リダイレクト処理を入れていきます。

/app/page.tsx
import { neon } from '@neondatabase/serverless';
+import { redirect } from 'next/navigation';

  // データベースへの接続
  async function create(formData: FormData) {
    'use server';

    // Connect to the Neon database
    const sql = neon(`${process.env.DATABASE_URL}`);
    const user = formData.get('name');
    const message = formData.get('message');
    // Insert the comment from the form into the Postgres database
    await sql('INSERT INTO messages ("user", message) VALUES ($1, $2)', [user, message]);

+   redirect('/');
  }
DaiNakaDaiNaka

これまでの成果

フォームから送信してみます。

うまく反映されたようです。

DaiNakaDaiNaka

バグ調査

ここまでのコードで、開発環境と本番環境で操作してみると、挙動がおかしいことが分かりました。
同じデータベースを使用しているため、開発環境で投稿されたメッセージもすべて表示されるはずが、本番環境には一部のメッセージしか表示されていませんでした。

  1. 本番環境でメッセージを送信すると、これまで投稿されたメッセージと新たに本番環境で投稿されたメッセージがすべて表示される
  2. 画面を更新するとそれらのメッセージが消えて元の状態(1.の前の状態)に戻ってしまう
  3. Vercelのダッシュボードで本番環境のキャッシュをクリアするとメッセージが全て復活する🤔

上記の挙動から本番環境のキャッシュが影響してそうなことは推測できましたが、コードが悪いのかデプロイしているVercelの環境設定が悪いのか、引き続き調査したいと思います。

revalidate(リバリデート)

有識者に「関係しているのはrevalidateではないか」とヒントをいただけた事もあり、公式ページもにらめっこしながら調べてみました。revalidateについては公式のチュートリアルでも書かれていたので、早速試してみました。(後に違うものだと発覚😢)

/app/page.tsx
import { neon } from '@neondatabase/serverless';
import { redirect } from 'next/navigation';
+import { revalidatePath } from 'next/cache';

     ・
     ・
     ・

  // データベースへの接続
  async function create(formData: FormData) {
    'use server';

    // Connect to the Neon database
    const sql = neon(`${process.env.DATABASE_URL}`);
    const user = formData.get('name');
    const message = formData.get('message');
    // Insert the comment from the form into the Postgres database
    await sql('INSERT INTO messages ("user", message) VALUES ($1, $2)', [user, message]);

+   revalidatePath('/');
    redirect('/');
  }

案の定、結果は変わりませんでした😢公式ページを見直しているとrevalidateと呼ばれるものを発見することができました。詳細は公式ページの説明を読んでいただけると良いかと思いますが、キャッシュに関するもので通常はfalseだが0などの数値を入れることでキャッシュの保持期限を設定できるようす。という事でコードに追加してみます。

/app/page.tsx
import { neon } from '@neondatabase/serverless';
import { redirect } from 'next/navigation';

+export const revalidate = 0; // キャッシュを無効化

export default async function Home() {
  const appName = process.env.APP_NAME;

     ・
     ・
     ・

上記のコードを追加することで、無事に本番環境でも送信されたメッセージが表示されるようになりました。

DaiNakaDaiNaka

Cookie操作

現状、連続してメッセージを送る場合は、毎回ユーザー名とメッセージを入れる必要があり、とても不便です。そこで、ユーザー名をクッキーに保存することでユーザー名を何度も入れる必要がないように変更したいと思います。

公式のCookieに関するページを参考にしています。
import { cookies } from 'next/headers';でCookie機能をインポートします。
const 変数 = (await cookies()).get('cookie_name');で変数にcookie_nameというCookie情報を読み込むことができます。
(await cookies()).set('cookie_name', 変数 as string, { expires: 期限 });で変数からcookie_nameというCookie情報を書き込むことができます。

実装

/app/page.tsx
import { neon } from '@neondatabase/serverless';
import { redirect } from 'next/navigation';
+import { cookies } from 'next/headers';

export const revalidate = 0; // キャッシュを無効化

export default async function Home() {
  const appName = process.env.APP_NAME;

  // データベースからメッセージを取得
  async function getMessages() {
    const sql = neon(`${process.env.DATABASE_URL}`);
    const rows = await sql('SELECT id, date, "user", message FROM messages');
    return rows;
  }

+   // 365日後の日付を取得
+   const expirationDate = new Date(Date.now() + 9 * 60 * 60 * 1000); // 日本時間を取得
+   expirationDate.setDate(expirationDate.getDate() + 365);

+ // Cookieを読み込み
+ const cookieStore = await cookies();
+ const nameCookie = cookieStore.get('user_name');
+ const name = nameCookie ? nameCookie.value : 'Guest';

  // データを取得
  const ITEMS = await getMessages();

  // データベースへの接続
  async function create(formData: FormData) {
    'use server';

    // Connect to the Neon database
    const sql = neon(`${process.env.DATABASE_URL}`);
    const user = formData.get('name');
    const message = formData.get('message');
    // Insert the comment from the form into the Postgres database
    await sql('INSERT INTO messages ("user", message) VALUES ($1, $2)', [user, message]);

+   // Cookieを設定
+   const cookieStore = await cookies();
+   cookieStore.set('user_name', user as string, { expires: expirationDate });
    
    redirect('/');
  }

  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">

      {/* Title */}
      <p className="text-2xl font-bold">
        {appName}
      </p>

      {/* Message area */}
      {ITEMS.map((item) => {
        return (
          <div key={item.id} className="mt-5">
            <div className="flex gap-2 text-xs">
              <p>{new Date(item.date).toLocaleString()}</p>
              <p>@{item.user}</p>
            </div>
            <p>{item.message}</p>
          </div>
        );
      })}

      {/* Form area */}
      <hr className="mt-5"/>

      <form className="mt-5 flex items-center" action={create}>
        <div className="flex items-center">
          <label>Name</label>
-         <input type="text" name="name" className="bg-gray-200 ml-2 p-1 w-24" autoFocus required />
+         <input type="text" name="name" className="bg-gray-200 ml-2 p-1 w-24" defaultValue={name} required />
        </div>
        
        <div className="ml-5 flex-1 flex items-center">
          <label>Message</label>
-         <input type="text" name="message" className="bg-gray-200 ml-2 p-1 flex-1" required/>
+         <input type="text" name="message" className="bg-gray-200 ml-2 p-1 flex-1" autoFocus required/>
        </div>

        <div>
          <button type="submit" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
            送信
          </button>
        </div>
      </form>

    </div>
  );
}

DaiNakaDaiNaka

これまでの成果

ユーザー名とメッセージが入っている状態で送信を実行します。

ユーザー名が残っていて、メッセージボックスにフォーカスされているので、連続してメッセージを送信することができるようになりました。

DaiNakaDaiNaka

リファクタ

リファクタとは、コードの可読性やメンテナンス性を高めるために、コンポーネント化したり、書き方などを見直すことを言います。これまでのコードは、インプットしながらという事もあり、全て/app/page.tsxに書いていたので、とても長くなってしまい可読性が下がっている状況のため、リファクタしました。

👇リファクタ前のpage.tsxのコード

/app/page.tsx
import { neon } from '@neondatabase/serverless';
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';

export const revalidate = 0; // キャッシュを無効化

export default async function Home() {
  const appName = process.env.APP_NAME;

  // データベースからメッセージを取得
  async function getMessages() {
    const sql = neon(`${process.env.DATABASE_URL}`);
    const rows = await sql('SELECT id, date, "user", message FROM messages');
    return rows;
  }

  // Cookieを読み込み
  const cookieStore = await cookies();
  const nameCookie = cookieStore.get('user_name');
  const name = nameCookie ? nameCookie.value : 'Guest';

  // データを取得
  const ITEMS = await getMessages();

  // データベースへの接続
  async function create(formData: FormData) {
    'use server';

    // Connect to the Neon database
    const sql = neon(`${process.env.DATABASE_URL}`);
    const user = formData.get('name');
    const message = formData.get('message');
    // Insert the comment from the form into the Postgres database
    await sql('INSERT INTO messages ("user", message) VALUES ($1, $2)', [user, message]);

    // 365日後の日付を取得
    const expirationDate = new Date(Date.now() + 9 * 60 * 60 * 1000); // 日本時間を取得
    expirationDate.setDate(expirationDate.getDate() + 365);

    // Cookieを設定
    const cookieStore = await cookies();
    cookieStore.set('user_name', user as string, { expires: expirationDate });
    
    redirect('/');
  }

  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">

      {/* Title */}
      <p className="text-2xl font-bold">
        {appName}
      </p>

      {/* Message area */}
      {ITEMS.map((item) => {
        return (
          <div key={item.id} className="mt-5">
            <div className="flex gap-2 text-xs">
              <p>{new Date(item.date).toLocaleString()}</p>
              <p>@{item.user}</p>
            </div>
            <p>{item.message}</p>
          </div>
        );
      })}

      {/* Form area */}
      <hr className="mt-5"/>

      <form className="mt-5 flex items-center" action={create}>
        <div className="flex items-center">
          <label>Name</label>
          <input type="text" name="name" className="bg-gray-200 ml-2 p-1 w-24" defaultValue={name} required />
        </div>
        
        <div className="ml-5 flex-1 flex items-center">
          <label>Message</label>
          <input type="text" name="message" className="bg-gray-200 ml-2 p-1 flex-1" autoFocus required/>
        </div>

        <div>
          <button type="submit" className="ml-3 py-1 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-200 text-gray-800 hover:bg-gray-200 focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none">
            送信
          </button>
        </div>
      </form>

    </div>
  );
}

👇リファクタ後のpage.tsxのコード

/app/page.tsx
import MessageArea from '@/app/ui/message-area';
import TitleArea from '@/app/ui/title-area';
import FormArea from './ui/form-area';

export const revalidate = 0; // キャッシュを無効化

export default async function Home() {
  return (
    <div className="w-[1000px] min-w-[600px] mx-auto grid bg-white mt-5 p-5 rounded-xl">
      <TitleArea />
      <MessageArea />
      <FormArea />
    </div>
  );
}

リファクタ前後のコードをご覧いただき、それぞれコンポーネント化されて可読性が上がっていることがお分かりいただけるかと思います。実際の各コードはGitHubをご覧いただけると助かります😊
https://github.com/DaiNaka1207/chat-app_nextjs

DaiNakaDaiNaka

公開ページ

Vercel.comに公開していますので、完成したチャットアプリはこんな感じというのを感じていただければと思います。

https://chat-app-nextjs-one.vercel.app/

このスクラップは3ヶ月前にクローズされました