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

はじめに
この節では、新しいスポットを投稿するフォーム画面 を作ります。
フォームとは、ユーザーが文字を入力したり、選択したりして、情報を送るための画面です。
たとえば、次のようなものです。
スポット名を入力する
説明文を入力する
カテゴリを選ぶ
場所を入力する
画像URLを入力する
投稿ボタンを押す
今回は、難しい保存処理までは深く入りません。
まずは、入力欄を作ること、入力された内容を受け取ること を目標にします。
この節のゴール
この節が終わると、次のことができるようになります。
- 新規投稿ページを作れる
- スポット名の入力欄を作れる
- 説明文の入力欄を作れる
- カテゴリの選択欄を作れる
- 場所と画像URLの入力欄を作れる
- 送信ボタンを配置できる
- 入力内容をまとめて受け取れる まずは、「入力欄を作って、ボタンで送る」 ところまでできれば十分です。
1. 今回作る画面
今回作る画面は、次のページです。
/spots/new
これは、新しいスポットを投稿する画面 です。
Next.js の App Router では、このファイルを作ります。
app/spots/new/page.tsx
フォルダ構成はこうです。
app
└─ spots
└─ new
└─ page.tsx
2. フォームで入力する内容
今回のフォームには、次の5つの入力項目を作ります。
| 項目 | 入力方法 | 例 |
|---|---|---|
| スポット名 | 1行入力 | 静かな図書館 |
| 説明文 | 複数行入力 | 勉強に集中しやすい場所です |
| カテゴリ | 選択 | カフェ、図書館、学食 |
| 場所 | 1行入力 | 名古屋駅 徒歩5分 |
| 画像URL | 1行入力 | 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" です。
これを付けると、ボタンを押したときに form の onSubmit が動きます。
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. 入力しても文字が表示されない
value と onChange が正しくつながっているか確認してください。
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です。
次は、入力した内容をリアルタイムで画面に反映する流れを学びます。
