Next.jsによるWebアプリケーション開発概論

フォーム入力の基本:新しいスポットを投稿する

5おすすめスポット共有アプリを作りながら学ぶ 画面遷移設計・状態管理・ユーザー入力処理
Next.jsTailwind CSSアプリ開発開発Web開発

はじめに

この節では、新しいスポットを投稿するフォーム画面 を作ります。

フォームとは、ユーザーが文字を入力したり、選択したりして、情報を送るための画面です。

たとえば、次のようなものです。

スポット名を入力する
説明文を入力する
カテゴリを選ぶ
場所を入力する
画像URLを入力する
投稿ボタンを押す

今回は、難しい保存処理までは深く入りません。

まずは、入力欄を作ること入力された内容を受け取ること を目標にします。


この節のゴール

この節が終わると、次のことができるようになります。

  • 新規投稿ページを作れる
  • スポット名の入力欄を作れる
  • 説明文の入力欄を作れる
  • カテゴリの選択欄を作れる
  • 場所と画像URLの入力欄を作れる
  • 送信ボタンを配置できる
  • 入力内容をまとめて受け取れる まずは、「入力欄を作って、ボタンで送る」 ところまでできれば十分です。

1. 今回作る画面

今回作る画面は、次のページです。

/spots/new

これは、新しいスポットを投稿する画面 です。

Next.js の App Router では、このファイルを作ります。

app/spots/new/page.tsx

フォルダ構成はこうです。

app
└─ spots
   └─ new
      └─ page.tsx

2. フォームで入力する内容

今回のフォームには、次の5つの入力項目を作ります。

項目入力方法
スポット名1行入力静かな図書館
説明文複数行入力勉強に集中しやすい場所です
カテゴリ選択カフェ、図書館、学食
場所1行入力名古屋駅 徒歩5分
画像URL1行入力https://example.com/image.jpg

この5つが入力できれば、投稿フォームとしてはかなり形になります。


3. まずページを作る

app/spots/new/page.tsx を作ってください。

app/spots/new/page.tsx

'use client';

import { useState } from 'react';

/**
 * 役割: 新しいスポットを投稿するフォーム画面を表示する
 * 入力: なし
 * 出力: 新規投稿フォーム画面
 */
export default function NewSpotPage(): JSX.Element {
  const [name, setName] = useState<string>('');
  const [description, setDescription] = useState<string>('');
  const [category, setCategory] = useState<string>('図書館');
  const [location, setLocation] = useState<string>('');
  const [imageUrl, setImageUrl] = useState<string>('');

  /**
   * 役割: フォーム送信時に入力内容をまとめて受け取る
   * 入力: event - フォーム送信イベント
   * 出力: なし
   */
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
    event.preventDefault();

    const newSpot = {
      name,
      description,
      category,
      location,
      imageUrl,
    };

    console.log('投稿内容:', newSpot);
    alert('投稿内容を受け取りました。コンソールを確認してください。');
  };

  return (
    <main style={{ padding: '32px', maxWidth: '640px', margin: '0 auto' }}>
      <h1>新しいスポットを投稿する</h1>
      <p>おすすめの場所を入力して、投稿してみましょう。</p>

      <form
        onSubmit={handleSubmit}
        style={{
          display: 'grid',
          gap: '16px',
          marginTop: '24px',
        }}
      >
        <label style={{ display: 'grid', gap: '8px' }}>
          <span>スポット名</span>
          <input
            type="text"
            value={name}
            onChange={(event) => {
              setName(event.target.value);
            }}
            placeholder="例)静かな図書館"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>説明文</span>
          <textarea
            value={description}
            onChange={(event) => {
              setDescription(event.target.value);
            }}
            placeholder="例)とても静かで、勉強や読書に集中できます。"
            rows={5}
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>カテゴリ</span>
          <select
            value={category}
            onChange={(event) => {
              setCategory(event.target.value);
            }}
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          >
            <option value="図書館">図書館</option>
            <option value="カフェ">カフェ</option>
            <option value="学食">学食</option>
            <option value="作業スペース">作業スペース</option>
            <option value="休憩スポット">休憩スポット</option>
          </select>
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>場所</span>
          <input
            type="text"
            value={location}
            onChange={(event) => {
              setLocation(event.target.value);
            }}
            placeholder="例)名古屋駅 徒歩5分"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>画像URL</span>
          <input
            type="url"
            value={imageUrl}
            onChange={(event) => {
              setImageUrl(event.target.value);
            }}
            placeholder="例)https://example.com/image.jpg"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <button
          type="submit"
          style={{
            padding: '12px 16px',
            border: 'none',
            borderRadius: '8px',
            background: '#08131a',
            color: '#fff',
            fontWeight: 700,
            cursor: 'pointer',
          }}
        >
          投稿する
        </button>
      </form>
    </main>
  );
}

4. 画面を確認する

ファイルを保存したら、開発サーバーを起動します。

npm run dev

ブラウザで次を開きます。

http://localhost:3000/spots/new

次のようなフォームが表示されればOKです。

新しいスポットを投稿する

スポット名
説明文
カテゴリ
場所
画像URL
投稿する

5. スポット名を入力する

まずは、スポット名の入力欄です。

<input
  type="text"
  value={name}
  onChange={(event) => {
    setName(event.target.value);
  }}
  placeholder="例)静かな図書館"
/>

ここで大事なのは、次の2つです。

value={name}

これは、入力欄に表示する値です。

onChange={(event) => {
  setName(event.target.value);
}}

これは、入力が変わったときに name を更新する処理です。

つまり、文字を入力すると、name の中身が変わります。


6. 説明文を入力する

説明文は、長くなることがあります。

そのため、input ではなく textarea を使います。

<textarea
  value={description}
  onChange={(event) => {
    setDescription(event.target.value);
  }}
  placeholder="例)とても静かで、勉強や読書に集中できます。"
  rows={5}
/>

textarea** を使う場面**

説明文、感想、メモのように、長めの文章を入力する場合に使います。


7. カテゴリを選ぶ

カテゴリは、自由入力ではなく、選択式にします。

<select
  value={category}
  onChange={(event) => {
    setCategory(event.target.value);
  }}
>
  <option value="図書館">図書館</option>
  <option value="カフェ">カフェ</option>
  <option value="学食">学食</option>
  <option value="作業スペース">作業スペース</option>
  <option value="休憩スポット">休憩スポット</option>
</select>

なぜ選択式にするのか

カテゴリを自由入力にすると、表記がバラバラになります。

たとえば、

カフェ
cafe
喫茶店
Cafe

のように、同じ意味でも別の文字になってしまいます。

選択式にすると、データがきれいにそろいます。


8. 画像URLや場所情報を入力する

場所は普通の文字入力で作ります。

<input
  type="text"
  value={location}
  onChange={(event) => {
    setLocation(event.target.value);
  }}
  placeholder="例)名古屋駅 徒歩5分"
/>

画像URLは type="url" にしておきます。

<input
  type="url"
  value={imageUrl}
  onChange={(event) => {
    setImageUrl(event.target.value);
  }}
  placeholder="例)https://example.com/image.jpg"
/>

画像URLとは

画像URLとは、画像の住所です。

たとえば、次のような文字です。

https://example.com/image.jpg

今回は、画像アップロードまでは行いません。

まずは、画像URLを入力する形にします。


9. 送信ボタンを配置する

フォームの最後に、送信ボタンを置きます。

<button type="submit">投稿する</button>

大事なのは、type="submit" です。

これを付けると、ボタンを押したときに formonSubmit が動きます。


10. フォーム送信の流れ

フォームは、このような流れで動きます。

ユーザーが入力する
↓
投稿するボタンを押す
↓
handleSubmit が動く
↓
入力内容をまとめる
↓
console.log で確認する

コードでは、この部分です。

const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
  event.preventDefault();

  const newSpot = {
    name,
    description,
    category,
    location,
    imageUrl,
  };

  console.log('投稿内容:', newSpot);
};

11. event.preventDefault() とは

フォーム送信では、何もしないとページが再読み込みされることがあります。

それを止めるのが、次のコードです。

event.preventDefault();

初心者のうちは、こう覚えてください。

フォームでは、最初に event.preventDefault() を書く。

これで、画面が勝手に再読み込みされるのを防げます。


12. 入力内容を確認する方法

投稿ボタンを押したら、ブラウザの開発者ツールを開きます。

Chrome の場合

  • 画面を右クリック
  • 「検証」を押す
  • 「Console」を開く

そこに、次のように表示されます。

投稿内容: { name: "...", description: "...", category: "...", location: "...", imageUrl: "..." }

これが表示されれば、フォームの入力内容を受け取れています。


13. よくあるつまずき

1. 入力しても文字が表示されない

valueonChange が正しくつながっているか確認してください。

value={name}
onChange={(event) => {
  setName(event.target.value);
}}

2. ボタンを押すと画面が再読み込みされる

event.preventDefault() があるか確認してください。

event.preventDefault();

3. useState が使えない

ファイルの一番上にこれがあるか確認してください。

'use client';

そして、useState を import しているか確認します。

import { useState } from 'react';

4. URLを開いてもページが出ない

ファイルの場所を確認してください。

正しい場所です。

app/spots/new/page.tsx

間違いやすい場所です。

app/spot/new/page.tsx
app/spots/new.tsx
components/spots/new/page.tsx

14. 必要最低限の完成コード

もう一度、完成コードをまとめておきます。

迷ったら、まずこのコードで動かしてください。

app/spots/new/page.tsx

'use client';

import { useState } from 'react';

/**
 * 役割: 新しいスポットを投稿するフォーム画面を表示する
 * 入力: なし
 * 出力: 新規投稿フォーム画面
 */
export default function NewSpotPage(): JSX.Element {
  const [name, setName] = useState<string>('');
  const [description, setDescription] = useState<string>('');
  const [category, setCategory] = useState<string>('図書館');
  const [location, setLocation] = useState<string>('');
  const [imageUrl, setImageUrl] = useState<string>('');

  /**
   * 役割: フォーム送信時に入力内容をまとめて受け取る
   * 入力: event - フォーム送信イベント
   * 出力: なし
   */
  const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
    event.preventDefault();

    const newSpot = {
      name,
      description,
      category,
      location,
      imageUrl,
    };

    console.log('投稿内容:', newSpot);
    alert('投稿内容を受け取りました。コンソールを確認してください。');
  };

  return (
    <main style={{ padding: '32px', maxWidth: '640px', margin: '0 auto' }}>
      <h1>新しいスポットを投稿する</h1>
      <p>おすすめの場所を入力して、投稿してみましょう。</p>

      <form
        onSubmit={handleSubmit}
        style={{
          display: 'grid',
          gap: '16px',
          marginTop: '24px',
        }}
      >
        <label style={{ display: 'grid', gap: '8px' }}>
          <span>スポット名</span>
          <input
            type="text"
            value={name}
            onChange={(event) => {
              setName(event.target.value);
            }}
            placeholder="例)静かな図書館"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>説明文</span>
          <textarea
            value={description}
            onChange={(event) => {
              setDescription(event.target.value);
            }}
            placeholder="例)とても静かで、勉強や読書に集中できます。"
            rows={5}
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>カテゴリ</span>
          <select
            value={category}
            onChange={(event) => {
              setCategory(event.target.value);
            }}
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          >
            <option value="図書館">図書館</option>
            <option value="カフェ">カフェ</option>
            <option value="学食">学食</option>
            <option value="作業スペース">作業スペース</option>
            <option value="休憩スポット">休憩スポット</option>
          </select>
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>場所</span>
          <input
            type="text"
            value={location}
            onChange={(event) => {
              setLocation(event.target.value);
            }}
            placeholder="例)名古屋駅 徒歩5分"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <label style={{ display: 'grid', gap: '8px' }}>
          <span>画像URL</span>
          <input
            type="url"
            value={imageUrl}
            onChange={(event) => {
              setImageUrl(event.target.value);
            }}
            placeholder="例)https://example.com/image.jpg"
            style={{
              padding: '12px',
              border: '1px solid #ccc',
              borderRadius: '8px',
            }}
          />
        </label>

        <button
          type="submit"
          style={{
            padding: '12px 16px',
            border: 'none',
            borderRadius: '8px',
            background: '#08131a',
            color: '#fff',
            fontWeight: 700,
            cursor: 'pointer',
          }}
        >
          投稿する
        </button>
      </form>
    </main>
  );
}

15. ここまでのまとめ

この節では、新しいスポットを投稿するフォーム を作りました。

今日覚えることは、これだけで十分です。

  • 1行入力は input
  • 長い文章は textarea
  • 選択式は select
  • 送信ボタンは button type="submit"
  • 入力値は useState で管理する
  • 送信時は onSubmit で受け取る
  • 最初に event.preventDefault() を書く

練習問題

問題1

スポット名のような短い文字を入力するときに使うHTMLタグは何ですか。

問題2

説明文のような長い文章を入力するときに使うHTMLタグは何ですか。

問題3

カテゴリを選ばせるときに使うHTMLタグは何ですか。

問題4

フォームの送信時に、画面の再読み込みを止めるために書くコードは何ですか。


回答

問題1の回答

input

問題2の回答

textarea

問題3の回答

select

問題4の回答

event.preventDefault();

次に進む前のチェック

次へ進む前に、次の5つを確認してください。

  • /spots/new を開ける
  • スポット名を入力できる
  • 説明文を入力できる
  • カテゴリを選べる
  • 投稿ボタンを押すと、コンソールに入力内容が出る ここまでできればOKです。

次は、入力した内容をリアルタイムで画面に反映する流れを学びます。

教材トップへ戻る