【🔰初心者向け】SNS型webアプリを作成しよう 第5回_Supabaseをセットアップして、投稿をデータベースに保存する【後編】

プログラミング

こんにちは。
長かった投稿機能実装もこれで最後です。今回は投稿フォームを作成していきましょう!
コードについて詳しく解説しているので、疑問点があれば適宜参照して下さい。

投稿フォームを作成

app/components/PostForm.tsx を作成し、以下のコードを入力:

"use client";

import { useState } from "react";
import { useAuth, useUser } from "@clerk/nextjs"; // Clerkの認証情報を取得

export default function PostForm() {
  const { getToken } = useAuth(); // ClerkのJWTトークンを取得する関数を取得
  const { user } = useUser(); // ユーザー情報を取得
  const [content, setContent] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setErrorMessage("");

    // ユーザーがログインしているかチェック
    if (!user) {
      setErrorMessage("ユーザーがログインしていません。");
      return;
    }

    // 投稿内容が空でないかチェック
    if (!content.trim()) {
      setErrorMessage("投稿内容を入力してください。");
      return;
    }

    try {
      const token = await getToken({ template: "supabase" }); // Clerk に対してユーザーごとに Supabase 用のトークンを取得するようにリクエスト

      if (process.env.NODE_ENV !== "production") {
        console.log("送信するトークン:", token);
      }

      const res = await fetch("/api/post", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${token}`,
        },
        body: JSON.stringify({ user_id: user.id, content }),
      });

      const data = await res.json();

      if (res.ok) {
        setContent(""); // ✅ 投稿成功時のみリセット
        alert("投稿成功!");
      } else {
        setErrorMessage(data.error || "投稿に失敗しました。");
      }
    } catch (error) {
      console.error("投稿エラー:", error);
      setErrorMessage("投稿処理中にエラーが発生しました。");
    }
  };

  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-2">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="投稿内容を入力..."
        className="border p-2"
      />
      <button type="submit" className="bg-blue-500 text-white p-2 rounded">
        投稿
      </button>
      {errorMessage && <p className="text-red-500">{errorMessage}</p>}
    </form>
  );
}

このコードでできること

  1. ユーザーがテキストを入力できる(textarea)
  2. 投稿ボタンを押すとデータを送る
  3. Clerk(認証サービス)でログインしているかチェック
  4. Supabase の API に投稿データを送信
  5. 投稿が成功すれば alert(“投稿成功!”) を表示
  6. エラーがあればエラーメッセージを表示
コードの意味を詳細に解説(クリックして展開)
"use client";

 「このコンポーネントはクライアント側(フロントエンド)で実行する」ことを Next.js に伝える
➡ Next.js では、デフォルトではサーバー側で処理されるため、「これはフロントエンド用だよ!」と明示するために use client をつける。


import { useState } from "react";
import { useAuth, useUser } from "@clerk/nextjs";
  • useState → 入力した投稿内容やエラーを管理するための React のフック
  • useAuth() → Clerk から「ログイン情報」を取得する(JWTトークンを取る)
  • useUser() → Clerk から「ユーザー情報」を取得する(ログインユーザーの id など)

  const { getToken } = useAuth(); // ClerkのJWTトークンを取得する関数を取得
  const { user } = useUser(); // ユーザー情報を取得
  const [content, setContent] = useState("");
  const [errorMessage, setErrorMessage] = useState("");
コード意味
const { getToken } = useAuth();Clerk の useAuth() から getToken() という関数を取得
const { user } = useUser();Clerk の useUser() から user を取得
const [content, setContent] = useState(“”);投稿内容を管理する state(変数)
const [errorMessage, setErrorMessage] = useState(“”);エラーメッセージを管理する state(変数)

useState の基本

useState は、「現在の値」と「値を更新する関数」をセットで返す 関数です。

const [state, setState] = useState(初期値);
役割説明
state現在の値(状態)
setStatestate の値を変更する関数
useState(初期値)状態の初期値をセットする

handleSubmitの定義

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  setErrorMessage("");
}

このコードは、投稿フォームの送信ボタンが押されたときの処理(handleSubmit) を定義している。

1. handleSubmit とは?

  • handleSubmit は関数(イベントハンドラー)で、フォームが送信されたときに実行される
  • 「async」付きなので、非同期処理(await を使う処理)を含む
  • フォーム送信のデフォルト動作を防ぎ、エラーメッセージをリセットする!

2. e.preventDefault(); の役割

これは「フォームのデフォルトの送信動作を防ぐ」ためのコード! 🚀

🟢 なぜ必要?

  • HTML の form 要素は、送信ボタンを押すと自動でページをリロードする動作がある!
  • React では JavaScript でデータを送るので、ページをリロードすると処理が止まってしまう!
  • だから、e.preventDefault(); を使ってリロードを防ぎ、React での処理を継続する!

📌 もし e.preventDefault(); をしないと?

  • フォームが送信されると、ページがリロードされる(React の状態が消えてしまう!)
  • 画面のデータ(useState で管理している content など)がリセットされてしまう!
  • 非同期処理(API へのリクエストなど)が動く前に、ページがリロードされてしまう!

e.preventDefault(); を入れることで、React の中でデータ送信処理を行える!

3. setErrorMessage(“”); の役割

これは「エラーメッセージをリセットする」ための処理!

🟢 なぜ必要?

  • もし前回の投稿でエラーが発生していた場合、errorMessage にエラーメッセージが残ってしまう。
  • 新しい投稿をする前に setErrorMessage(“”) で エラーメッセージを消してリセットする!

サーバーに投稿データを送信するためのリクエスト(fetch)を実行

const res = await fetch("/api/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${token}`,
  },
  body: JSON.stringify({ user_id: user.id, content }),
});

このコードの流れ

  1. fetch() が POST リクエストを /api/post に送る
  2. サーバーがリクエストを処理し、レスポンス(応答)を返す
  3. そのレスポンスが res に格納される

このコードの fetch() は、次のことをしている

  • /api/post という サーバー側のエンドポイント(API) にデータを送る
  • POST リクエストを送る(新しいデータを作成・追加するときに使う)
  • headers(リクエストの設定)を追加して、サーバーに正しく情報を伝える
  • body(送るデータの中身)を JSON 形式に変換して送信

1. method: “POST” の意味

method: "POST"

📌 リクエストの種類(HTTPメソッド)を「POST」に指定している!

メソッド何をする?
GETデータを取得する(読み取る)
POST新しいデータを追加する
PUTデータを更新する
DELETEデータを削除する

このコードでは POST を使って、新しい投稿(content)をサーバーに追加する! 🚀

2. headers: { … } の意味

headers: {
  "Content-Type": "application/json",
  Authorization: `Bearer ${token}`,
}

この headers は「サーバーに送るデータの設定」です
サーバーが「どういうデータが送られてくるのか?」を正しく認識するために使います

✅ “Content-Type”: “application/json” の意味

📌 「このリクエストのデータは JSON 形式ですよ!」とサーバーに伝える設定

Authorization: “Bearer ${token}” の意味

📌 「このリクエストはログイン済みのユーザーからのものですよ!」と証明するための認証情報

💡 ポイント

  • Bearer(ベアラー)= 「このトークンを使って認証しますよ!」の意味
  • ${token} = getToken({ template: “supabase” }) で取得したトークン
  • サーバー側はこのトークンを検証し、「本当にログイン済みのユーザーか?」をチェックする!

この Authorization ヘッダーをつけることで、認証が必要な API にアクセスできる! 🔥

3. body: JSON.stringify({ user_id: user.id, content }) の意味

body: JSON.stringify({ user_id: user.id, content })

📌 リクエストの「本体(送るデータ)」を JSON 形式に変換して送信!

✅ { user_id: user.id, content } の部分

  • user_id: user.id「どのユーザーが投稿したのか?」
  • content「投稿の内容(テキスト)」

📌 これを JSON 形式(文字列)に変換して送信する!

✅ JSON.stringify() の役割

  • JavaScript のオブジェクト {} を JSON 文字列 “{…}” に変換する!
  • サーバー側で req.json() を使って解析できるようにする!

投稿処理の結果を確認

const data = await res.json();

if (res.ok) {
  setContent(""); // ✅ 投稿成功時のみリセット
  alert("投稿成功!");
} else {
  setErrorMessage(data.error || "投稿に失敗しました。");
}
} catch (error) {
  console.error("投稿エラー:", error);
  setErrorMessage("投稿処理中にエラーが発生しました。");
}

1. const data = await res.json();

📌 サーバーから返ってきたレスポンスを JSON に変換して data に格納する
📌 この data の中には、サーバーが返した message や error などの情報が入っている

2. if (res.ok) { … } 成功時の処理

if (res.ok) {
  setContent(""); // ✅ 投稿成功時のみリセット
  alert("投稿成功!");
}

📌 res.ok は「リクエストが成功したか?」を判定する

  • 成功(res.ok === true)なら、投稿フォームの内容をリセット
  • 「投稿成功!」のアラートを表示

🟢 例:投稿が成功した場合

  • サーバーが 200 OK(成功)を返す
  • res.ok === true になる
  • setContent(“”) で投稿フォームを空にする
  • alert(“投稿成功!”) でユーザーに通知

「成功したら、投稿フォームをリセットして、成功メッセージを表示する」 🚀

3. else { … } 失敗時の処理

else {
  setErrorMessage(data.error || "投稿に失敗しました。");
}

📌 res.ok === false(エラー発生)の場合は、エラーメッセージを表示する

🟢 data.error があれば、その内容を表示
サーバーが “error”: “タイトルが空です” を返した場合、setErrorMessage(“タイトルが空です”)

🔴 data.error がない場合
“投稿に失敗しました。” というデフォルトのメッセージを表示

「エラーがあればその内容を表示、なければデフォルトのエラーメッセージを出す!」

4. catch (error) { … } ネットワークエラー時の処理

} catch (error) {
  console.error("投稿エラー:", error);
  setErrorMessage("投稿処理中にエラーが発生しました。");
}

📌 try 内で何かしらのエラー(例:ネットワーク障害、サーバーが落ちているなど)が起きたら、catch に移動する


投稿フォームの UI(見た目)を定義

return (
  <form onSubmit={handleSubmit} className="flex flex-col gap-2">
    <textarea
      value={content}
      onChange={(e) => setContent(e.target.value)}
      placeholder="投稿内容を入力..."
      className="border p-2"
    />
    <button type="submit" className="bg-blue-500 text-white p-2 rounded">
      投稿
    </button>
    {errorMessage && <p className="text-red-500">{errorMessage}</p>}
  </form>
);

1. <form>(フォーム全体)

<form onSubmit={handleSubmit} className="flex flex-col gap-2">

📌 <form> は「投稿のためのフォーム」を作るタグ
📌 onSubmit={handleSubmit} → フォームを送信したときに handleSubmit を実行する
📌 className=”flex flex-col gap-2″ → Tailwind CSS を使ってデザインを設定

🟢 handleSubmit の動作

  • 「投稿」ボタンを押すと、handleSubmit 関数が実行される
  • e.preventDefault(); により、ページのリロードを防ぐ
  • 投稿内容を API に送信する

この <form> は「投稿ボタンを押したときに、非同期で投稿処理を実行するための仕組み」

2. <textarea>(投稿内容の入力)

<textarea
  value={content}
  onChange={(e) => setContent(e.target.value)}
  placeholder="投稿内容を入力..."
  className="border p-2"
/>

📌 <textarea> は「投稿内容を入力するためのボックス」
📌 ユーザーが入力すると content の状態が更新される

✅ value={content}

  • content(投稿内容のデータ)が textarea に表示される
  • 初期値は “”(空)だが、ユーザーが入力すると更新される

✅ onChange={(e) => setContent(e.target.value)}

  • e は「イベントオブジェクト(event)」
  • onChange イベントが発生したとき、自動で e(イベントの情報)が渡される
  • ユーザーが文字を入力するたびに setContent(e.target.value) が実行される
  • content の状態が更新され、画面の textarea に即時反映される
コード意味
eイベントオブジェクト(onChange が発生したときの情報)
e.targetイベントが発生した要素(この場合は <textarea>)
e.target.valueユーザーが入力した内容(textarea の値)
setContent(e.target.value)content の状態を更新し、入力内容を保存!

この <textarea> は「ユーザーが投稿内容を入力し、そのデータを content に反映する仕組み」

3. <button>(投稿ボタン)

<button type="submit" className="bg-blue-500 text-white p-2 rounded">
  投稿
</button>

📌 <button type=”submit”> → フォームを送信するボタン
📌 className=”bg-blue-500 text-white p-2 rounded” → Tailwind CSS でボタンのデザインを設定

✅ type=”submit”

  • ボタンを押すと、<form> の onSubmit(= handleSubmit)が実行される
  • つまり、「投稿」ボタンを押すと handleSubmit が動いて、投稿が送信される

この <button> は「投稿を送信するためのボタン」

4. {errorMessage && <p className=”text-red-500″>{errorMessage}</p>}(エラーメッセージ表示)

{errorMessage && <p className="text-red-500">{errorMessage}</p>}

📌 エラーメッセージがあるときだけ、エラーメッセージを表示する
📌 className=”text-red-500″ → Tailwind CSS でエラー文字を赤色にする

✅ errorMessage && <p>{errorMessage}</p> の意味

  • errorMessage“”(空)のとき → 何も表示しない
  • errorMessage“投稿に失敗しました” が入っているとき → 画面にエラーメッセージを表示

投稿フォームをページに表示

src/app/page.tsx を修正

import { SignInButton, SignOutButton, SignedIn, SignedOut } from "@clerk/nextjs";
import PostForm from "./components/PostForm";

export default function Home() {
  return (
    <main className="flex flex-col items-center justify-center min-h-screen">
      <h1 className="text-2xl font-bold mb-4">OriginalSNS</h1>
      <PostForm />

      <SignedOut>
        <SignInButton />
      </SignedOut>

      <SignedIn>
        <p>ログイン済み</p>
        <SignOutButton />
      </SignedIn>
    </main>
  );
}

現在のディレクトリ構造

現在のディレクトリ構造は以下の通りです。エラーが出る場合はこれを見てディレクトリの構造を確認して下さい。

my-app/
│── .next/                  # Next.js のビルドキャッシュ(気にしなくてOK)
│── node_modules/           # インストールされたパッケージ(気にしなくてOK)
│── public/                 # 公開用の静的ファイル(画像など)
│── src/                    # ここからが実際のコード
│   ├── app/                # Next.jsのルーティング(各ページ)
│   │   ├── page.tsx        # ホームページ(トップページ)
│   │   ├── api/            # APIルート(サーバーサイド処理)
│   │   │   ├── post/     # 投稿API
│   │   │   │   ├── route.ts  # 投稿データを処理するAPI
│   │   ├── components/      # 再利用可能なUIコンポーネント
│   │   │   ├── PostForm.tsx # 投稿フォーム
│   ├── lib/                 # データベース接続やユーティリティ
│   │   ├── supabase.ts      # Supabaseの設定
│   ├── middleware.ts.      # 認証処理
│── .env.local               # 環境変数(APIキー)
│── package.json             # プロジェクトの設定
│── tsconfig.json            # TypeScriptの設定
│── next.config.js           # Next.jsの設定

動作確認

ターミナルから開発サーバーを再起動して、動作を確認します。

npm run dev
  • http://localhost:3000 でフォームにテキストを入力し、投稿ボタンを押す。
  • Supabase にデータが保存される
  • Supabase → 「Table Editor」「post」投稿内容が表示されていればOK!

お疲れ様でした、これでついに投稿とデータベースへの保存ができるようになりました!
次回は投稿の一覧表示を実装していきましょう。

コメント

タイトルとURLをコピーしました