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

useStateで学ぶ状態管理:入力内容をリアルタイムに反映する

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

はじめに

この節では、React の useState を使って、入力した内容をすぐ画面に反映する方法 を学びます。

前の節では、フォームに入力して、投稿ボタンを押したときに内容を受け取るところまで作りました。

今回は、入力している途中から、右側のプレビュー画面に内容が表示されるようにします。

たとえば、スポット名に「静かな図書館」と入力すると、すぐにプレビューにも「静かな図書館」と表示されます。

これができると、アプリが一気に「動いている感じ」になります。


この節のゴール

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

  • 入力した値を useState で保持できる
  • 入力内容をプレビュー画面に表示できる
  • 入力が空のときだけ別の文字を出せる
  • 画像URLがあるときだけ画像を表示できる
  • 「状態」という考え方を体験で理解できる 今日の合言葉はこれです。

入力する → state に入る → 画面に表示される

これだけ覚えれば大丈夫です。


1. 状態とは何か

まず、「状態」という言葉をかんたんにします。

状態とは、今の画面が覚えている値 です。

たとえば、フォームでは次のようなものが状態です。

スポット名に何が入力されているか
説明文に何が入力されているか
カテゴリで何が選ばれているか
画像URLが入力されているか

つまり、状態は 画面の中にある一時的なメモ のようなものです。


たとえで考える

入力欄に「静かな図書館」と入力します。

入力欄
↓
useState が覚える
↓
画面に表示する

この「覚えている場所」が state です。


2. useState の基本

useState は、React で値を覚えるための道具です。

基本の形はこれです。

const [値, 値を変更する関数] = useState(最初の値);

実際には、こんな形で使います。

const [name, setName] = useState<string>('');

これは、次の意味です。

部分意味
name現在のスポット名
setNameスポット名を変更する関数
useState<string>('')最初は空文字で始める

まず覚える形

最初は、この形をそのまま覚えてください。

const [name, setName] = useState<string>('');

name は読むため。setName は書き換えるため。

そんな感じで大丈夫です。


3. 入力した値を保持する

入力欄と state をつなぐには、valueonChange を使います。

<input
  type="text"
  value={name}
  onChange={(event) => {
    setName(event.target.value);
  }}
/>

これで、入力欄に文字を入れるたびに、name が更新されます。


動きの流れ

流れはこうです。

1. ユーザーが文字を入力する
2. onChange が動く
3. setName が呼ばれる
4. name の値が変わる
5. 画面が新しい値で表示される

ここで大切なのは、state が変わると画面も更新される ことです。


4. プレビュー画面に反映する

ここからが今回のメインです。

入力した name を、プレビューにも表示します。

<h2>{name}</h2>

これだけで、入力した文字が画面に出ます。

ただし、最初は name が空です。

そのままだと何も表示されません。

そこで、空のときだけ別の文字を出します。

<h2>{name || 'スポット名がここに表示されます'}</h2>

||** の意味**

|| は、「左が空なら右を使う」という書き方です。

{name || 'スポット名がここに表示されます'}

これは、こういう意味です。

name に文字がある
→ name を表示する

name が空
→ 「スポット名がここに表示されます」を表示する

初心者のうちは、空のときの代わりの文字 を出す書き方だと思ってください。


5. 入力に応じて表示を切り替える

次は、画像URLです。

画像URLが入力されているときだけ、画像を表示したいです。

入力されていないときは、「画像プレビュー」と表示します。

{imageUrl.length > 0 ? (
  <img src={imageUrl} alt={name} />
) : (
  <div>画像プレビュー</div>
)}

? :** の意味**

これは、条件によって表示を切り替える書き方です。

条件 ? A : B

意味はこうです。

条件が true なら A
条件が false なら B

今回なら、こうです。

画像URLがある
→ 画像を表示

画像URLがない
→ 「画像プレビュー」と表示

6. 今回作る完成イメージ

今回は、フォームの下にプレビューを付けます。

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

[スポット名入力]
[説明文入力]
[カテゴリ選択]
[場所入力]
[画像URL入力]
[投稿する]

----------------

プレビュー
入力した画像
入力したスポット名
入力した説明文
カテゴリ
場所

フォームに入力すると、プレビューがリアルタイムに変わります。


7. 完成コード

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={{
        maxWidth: '960px',
        margin: '0 auto',
        padding: '32px',
      }}
    >
      <h1>新しいスポットを投稿する</h1>
      <p>入力した内容が、下のプレビューにリアルタイムで表示されます。</p>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: '24px',
          marginTop: '24px',
        }}
      >
        <form
          onSubmit={handleSubmit}
          style={{
            display: 'grid',
            gap: '16px',
          }}
        >
          <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>

        <section
          style={{
            border: '1px solid #ddd',
            borderRadius: '12px',
            padding: '16px',
            background: '#fff',
          }}
        >
          <h2>プレビュー</h2>

          <div
            style={{
              marginTop: '16px',
              border: '1px solid #eee',
              borderRadius: '12px',
              overflow: 'hidden',
            }}
          >
            {imageUrl.length > 0 ? (
              <img
                src={imageUrl}
                alt={name || 'スポット画像'}
                style={{
                  width: '100%',
                  height: '180px',
                  objectFit: 'cover',
                  display: 'block',
                }}
              />
            ) : (
              <div
                style={{
                  height: '180px',
                  display: 'grid',
                  placeItems: 'center',
                  background: '#f5f8fa',
                  color: '#666',
                }}
              >
                画像URLを入力すると表示されます
              </div>
            )}

            <div style={{ padding: '16px' }}>
              <p
                style={{
                  display: 'inline-block',
                  padding: '4px 10px',
                  borderRadius: '999px',
                  background: '#e6f6f2',
                  color: '#1e7b65',
                  fontSize: '12px',
                  fontWeight: 700,
                }}
              >
                {category}
              </p>

              <h3 style={{ marginTop: '12px' }}>
                {name || 'スポット名がここに表示されます'}
              </h3>

              <p style={{ lineHeight: 1.8 }}>
                {description || '説明文を入力すると、ここに表示されます。'}
              </p>

              <p style={{ color: '#666' }}>
                場所: {location || '場所を入力してください'}
              </p>
            </div>
          </div>
        </section>
      </div>
    </main>
  );
}

8. 画面を確認する

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

npm run dev

ブラウザで開きます。

http://localhost:3000/spots/new

次のことを試してください。

  • スポット名を入力する
  • 説明文を入力する
  • カテゴリを変える
  • 場所を入力する
  • 画像URLを入力する

入力すると、右側のプレビューが変われば成功です。


9. 画像URLのテスト用サンプル

画像が表示されるか試すために、次のURLを画像URL欄に入れてみてください。

https://images.unsplash.com/photo-1521587760476-6c12a4b040da?auto=format&fit=crop&w=1200&q=80

画像が表示されればOKです。


10. どこでリアルタイム反映しているのか

スポット名は、ここで保存しています。

const [name, setName] = useState<string>('');

入力欄で、ここを更新しています。

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

プレビューで、ここを表示しています。

{name || 'スポット名がここに表示されます'}

つまり、流れはこうです。

入力欄に文字を入れる
↓
setName が動く
↓
name が変わる
↓
プレビューの表示が変わる

11. 入力に応じて表示を切り替える例

この部分を見てください。

{imageUrl.length > 0 ? (
  <img src={imageUrl} alt={name || 'スポット画像'} />
) : (
  <div>画像URLを入力すると表示されます</div>
)}

これは、画像URLがあるかどうかで表示を切り替えています。

画像URLがある
→ 画像を表示

画像URLがない
→ 案内文を表示

これも状態管理のよくある使い方です。


12. 初心者がよく間違えるところ

1. useState を import していない

ファイルの上に必要です。

import { useState } from 'react';

2. 'use client'; を書いていない

useState を使うファイルでは、上に書きます。

'use client';

3. valueonChange がつながっていない

入力欄には、この2つが必要です。

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

4. state を直接書き換えようとする

これはよくありません。

name = '静かな図書館';

state は、必ず setName で変えます。

setName('静かな図書館');

13. 今回覚えること

今回のポイントは、かなり少ないです。

  • useState は値を覚えるためのもの
  • setName のような関数で値を変える
  • state が変わると画面が更新される
  • 入力欄とプレビューは同じ state を見ている
  • 条件によって表示を切り替えられる

14. ここまでのまとめ

この節では、useState を使って、入力内容をリアルタイムにプレビューへ反映しました。

大切なのは、次の流れです。

入力する
↓
state に保存される
↓
画面に表示される

この流れがわかれば、React の状態管理の第一歩はクリアです。


練習問題

問題1

useState は何をするために使いますか。

問題2

入力欄の値が変わったときに動くイベントは何ですか。

問題3

name の値を変えるときに使う関数は何ですか。

問題4

画像URLがあるときだけ画像を表示したい場合、何を使って表示を切り替えますか。


回答

問題1の回答

値を覚えるために使います。

この教材では、入力されたスポット名や説明文を覚えるために使います。

問題2の回答

onChange

問題3の回答

setName

問題4の回答

条件分岐を使います。

今回のコードでは、次のような書き方を使いました。

imageUrl.length > 0 ? 画像を表示 : 案内文を表示

次に進む前のチェック

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

  • useState を使って入力値を保存できた
  • 入力欄に文字を入れるとプレビューが変わる
  • カテゴリを変えるとプレビューも変わる
  • 画像URLを入れると画像が表示される
  • state が変わると画面も変わる、という感覚がわかった

ここまでできればOKです。次は、検索や絞り込みなど、一覧画面をもっと便利にする機能へ進めます。

教材トップへ戻る