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

はじめに
この節では、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 をつなぐには、value と onChange を使います。
<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. value と onChange がつながっていない
入力欄には、この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です。次は、検索や絞り込みなど、一覧画面をもっと便利にする機能へ進めます。
