Skip to main content

第06章:画面を作ろう:ボタン・入力欄・表示エリア 🎨

この章では、いったん API 接続はいったん横に置いて、React 側の見た目をしっかり作ります 😊 Cloudflare の公式導線は、React SPA と Workers API を create-cloudflare と Cloudflare Vite plugin でまとめて始める形になっていて、ローカル開発でも Workers runtime にかなり近い挙動を確認しやすいのが強みです。だからこそこの章では、「あとで Worker とつなぎやすい UI」を先に作るのがすごく理にかなっています。 (Cloudflare Docs)

React 公式でも、イベント処理、入力欄、条件分岐、配列からの一覧表示、状態管理が UI の基本として整理されています。第6章では、その中でも特に「押せる」「入力できる」「表示が切り替わる」「履歴が並ぶ」の4つに絞って、小さなアプリの見た目を完成に近づけます。 (React)

なお、React 19 はすでに安定版で、useActionState<form action> など新しいフォーム系機能もあります。ただ、この章ではあえてそこへ飛び込まず、useStateonChangeonClick で「画面がどう動くか」をまず自分の手で理解する方を優先します。基礎が見えると、その後の新機能もずっと理解しやすくなります 🌱 (React)


この章のゴール 🎯

この章が終わるころには、こんな画面を自力で作れるようになるのが目標です。

  • タイトルと説明文がある
  • テキスト入力欄がある
  • モードを選ぶプルダウンがある
  • ボタンを押すと表示エリアが切り替わる
  • 読み込み中メッセージが出る
  • 結果カードが表示される
  • 直前の履歴が一覧で並ぶ

ここで作る UI は、次章で Workers API とつなぎ、その先では Workers AI につなぐ前提の「土台」になります。Workers AI は Cloudflare のネットワーク上でサーバーレスに AI モデルを呼べる仕組みで、Workers から使えるので、今のうちに「入力欄」「送信ボタン」「結果カード」を整えておく意味が大きいです。 (Cloudflare Docs)


この章で作る題材 🪄🤖

今回は、あとで AI 要約や AI 返信につなげやすいように、「ひとこと相談 UI」 を作ることにします。

まだ Cloudflare Worker は呼びません。 でも見た目はちゃんとアプリっぽくします ✨

画面の流れはこんな感じです。

  1. ユーザーが相談文を入力する
  2. 返答トーンを選ぶ
  3. ボタンを押す
  4. いったん「考え中…」を出す
  5. 仮の結果カードを表示する
  6. 履歴にも残す

この設計にしておくと、後で「仮の結果」を API の実レスポンスに差し替えるだけで、かなり自然に Cloudflare アプリへ育てられます 😊


まず理解したい React の考え方 🧠✨

React では、ボタンを押した、文字を入力した、送信中になった、結果が返った、という変化を state で持ち、見た目はその state に応じて切り替えます。React 公式でも、UI を作るときは「どんな見た目の状態があるか」を先に考え、それを useState で表す流れが紹介されています。 (React)

また、React の入力系コンポーネントである <input><textarea><select> は、value を渡すと「制御された入力」として扱われます。これを使うと、「今どんな文字が入っているか」「選択中のモードは何か」を React 側で安全に持てるので、学習段階ではとてもおすすめです。 (React)


画面の完成イメージを先に決めよう 🖼️

先に「状態」を決めると、コードがかなり書きやすくなります。

今回の state はこの5つで十分です。

  • prompt … 入力欄の文字
  • tone … 返答トーン
  • isLoading … 読み込み中かどうか
  • result … 表示する結果文
  • history … 過去の結果一覧

React 公式でも、state は「必要最小限」にして、重複や矛盾を避けるのが大切だと説明されています。たとえば「表示中の結果」と「履歴の先頭」に同じ情報を無理に二重管理すると、あとでズレやすくなります。 (React)


まずは App.tsx を作ろう 💻

src/App.tsx をこんな形にしてみましょう。

import { useState } from "react";
import "./App.css";

type HistoryItem = {
id: number;
prompt: string;
tone: string;
preview: string;
};

export default function App() {
const [prompt, setPrompt] = useState("");
const [tone, setTone] = useState("やさしく");
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState("");
const [history, setHistory] = useState<HistoryItem[]>([]);

async function handlePreview() {
const trimmed = prompt.trim();
if (!trimmed) return;

setIsLoading(true);
setResult("");

await new Promise((resolve) => setTimeout(resolve, 700));

const preview = `${tone}モード】「${trimmed}」への仮の返答です。ここは次章で Workers API の結果に置き換わります。`;

setResult(preview);
setHistory((prev) => [
{
id: Date.now(),
prompt: trimmed,
tone,
preview,
},
...prev,
].slice(0, 5));

setIsLoading(false);
}

function handleClear() {
setPrompt("");
setTone("やさしく");
setResult("");
setIsLoading(false);
}

return (
<main className="page">
<section className="app-card">
<p className="eyebrow">React × Cloudflare mini app</p>
<h1>ひとこと相談プレビュー 🤖</h1>
<p className="lead">
まずは見た目だけ作ります。あとで Cloudflare Workers とつなぎます。
</p>

![UI Structure](./picture/cloudflare_react_base_ts_study_006_ui_structure.png)

<div className="form-block">
<label htmlFor="prompt" className="label">
相談文 ✍️
</label>
<textarea
id="prompt"
className="textarea"
rows={5}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="例:勉強のやる気が出ないとき、どう始めればいい?"
/>
</div>

<div className="form-block">
<label htmlFor="tone" className="label">
返答トーン 🎭
</label>
<select
id="tone"
className="select"
value={tone}
onChange={(e) => setTone(e.target.value)}
>
<option value="やさしく">やさしく</option>
<option value="ていねいに">ていねいに</option>
<option value="短めに">短めに</option>
<option value="前向きに">前向きに</option>
</select>
</div>

<div className="button-row">
<button
className="primary-button"
onClick={handlePreview}
disabled={isLoading || !prompt.trim()}
>
{isLoading ? "考え中..." : "プレビューする 🚀"}
</button>

<button className="secondary-button" onClick={handleClear}>
クリア
</button>
</div>

<section className="result-area" aria-busy={isLoading}>
<h2>表示エリア 📦</h2>

{!isLoading && !result && (
<p className="muted">
まだ結果はありません。入力してボタンを押してみましょう。
</p>
)}

{isLoading && <p className="loading">AIが考えている雰囲気を演出中... ⏳</p>}

{!isLoading && result && (
<article className="result-card">
<h3>今回の結果 ✨</h3>
<p>{result}</p>
</article>
)}
</section>

<section className="history-area">
<h2>履歴 📝</h2>

{history.length === 0 ? (
<p className="muted">履歴はまだありません。</p>
) : (
<ul className="history-list">
{history.map((item) => (
<li key={item.id} className="history-card">
<p className="history-tone">トーン:{item.tone}</p>
<p className="history-prompt">入力:{item.prompt}</p>
<p className="history-preview">{item.preview}</p>
</li>
))}
</ul>
)}
</section>
</section>
</main>
);
}

このコードには、第6章で覚えたい要素がかなり詰まっています。 onChange で入力を受け取り、onClick でイベントを発火し、isLoadingresult によって表示エリアを条件分岐しています。こうしたイベント処理、制御された入力、条件付き表示、配列からの一覧表示は、React の基本パターンそのものです。 (React)


次に App.css で見た目を整えよう 🎀

:root {
font-family: "Segoe UI", "Meiryo", sans-serif;
color: #1f2937;
background: #f7f9fc;
}

* {
box-sizing: border-box;
}

body {
margin: 0;
}

.page {
min-height: 100vh;
padding: 32px 16px;
display: grid;
place-items: start center;
}

.app-card {
width: min(100%, 760px);
background: white;
border: 1px solid #e5e7eb;
border-radius: 20px;
padding: 24px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.06);
}

.eyebrow {
margin: 0 0 8px;
font-size: 12px;
color: #6366f1;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}

h1 {
margin: 0 0 12px;
font-size: 32px;
}

h2 {
margin-top: 0;
font-size: 20px;
}

.lead {
margin: 0 0 24px;
color: #4b5563;
}

.form-block {
margin-bottom: 18px;
}

.label {
display: block;
margin-bottom: 8px;
font-weight: 700;
}

.textarea,
.select {
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 12px 14px;
font-size: 16px;
background: #fff;
}

.textarea {
resize: vertical;
min-height: 120px;
}

.button-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
margin: 20px 0 28px;
}

.primary-button,
.secondary-button {
border: none;
border-radius: 999px;
padding: 12px 18px;
font-size: 15px;
cursor: pointer;
}

.primary-button {
background: #4f46e5;
color: white;
}

.primary-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.secondary-button {
background: #e5e7eb;
color: #111827;
}

.result-area,
.history-area {
margin-top: 24px;
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 16px;
background: #fafafa;
}

.result-card,
.history-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 14px;
}

.loading {
color: #7c3aed;
font-weight: 700;
}

.muted {
color: #6b7280;
}

.history-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 12px;
}

.history-tone {
margin: 0 0 6px;
font-size: 13px;
font-weight: 700;
color: #2563eb;
}

.history-prompt {
margin: 0 0 6px;
font-weight: 600;
}

.history-preview {
margin: 0;
color: #374151;
}

React 19 では <title><meta> などの扱いも以前より強化されていますが、この章ではそこまで広げず、まずは普通の HTML 要素を React で組み立てる感覚をつかめば十分です。React は標準の HTML / SVG 要素をそのまま扱えるので、<main><section><label><button> を素直に使うだけでもかなり読みやすい UI になります。 (React)


このコードの「学びどころ」を分解しよう 🔍

1. 入力欄は valueonChange でつなぐ ✍️

textareaselectvalue を渡し、onChange で state を更新しています。 これは React でよく使う controlled component の形です。

メリットはとても大きいです。

  • 今の入力値をすぐ読める
  • ボタン無効化に使える
  • 送信前チェックに使える
  • 後で Worker にそのまま渡せる

React の入力コンポーネントは、まさにこの「制御された形」を前提に理解するとすごく分かりやすくなります。 (React)

Controlled Input Flow

2. ボタンは「押せるだけ」で終わらせない 🖱️

今回は onClick={handlePreview} として、押されたら state が変わるようにしています。 React ではイベントハンドラは JSX に直接結びつけられ、クリック、入力、フォーカスなどの反応を関数として整理できます。UI が「静止画」から「アプリ」になる最初の瞬間です。 (React)

Button Event Flow

3. ローディング表示は isLoading で管理する ⏳

通信していなくても、今回はあえて待ち時間を作ってローディング表示を出しています。 これは本物の API につないだときの形に近づけるためです。React 19 には pending state を扱いやすくする新しい仕組みもありますが、まずは isLoading を自分で持つ形が一番分かりやすいです。 (React)

4. 表示エリアは「条件分岐」で切り替える 🪄

React では、条件に応じて JSX を出し分けるのに if&&、三項演算子 ? : など普通の JavaScript を使います。 今回の画面でも、

  • まだ結果なし
  • 読み込み中
  • 結果あり

の3状態を切り替えています。これができると、一気にアプリらしく見えます。 (React)

Conditional Rendering Logic

5. 履歴一覧は map() で表示する 📚

履歴は配列で持ち、history.map(...) でカードを並べています。 React の一覧表示はこの形が基本です。あとで API から複数件のデータが返ってきても、同じ考え方でそのまま伸ばせます。 (React)

Array Mapping to UI

6. state は増やしすぎない 🧺

今回 resulthistory は持っていますが、たとえば hasResult のような「result を見れば分かる値」は別 state にしていません。 React 公式でも、state は重複・矛盾・無駄な増殖を避ける方がバグが減ると案内されています。初心者のうちは「本当に必要なものだけ state に入れる」を強く意識するとかなり楽です。 (React)


Cloudflare 目線でこの画面がえらい理由 ☁️💡

Cloudflare の React + Vite 導線では、フロント側の src/App.tsx から Worker の API を呼ぶ流れが最初から示されています。つまり UI をこの段階できれいに分けておくと、次章では handlePreview() の中身だけを「仮の文字列生成」から「fetch("/api/...")」へ置き換えればよくなります。これは学習効率がかなり良いです。 (Cloudflare Docs)

Worker Gateway Conceptual Flow

さらに Cloudflare Vite plugin は、Workers runtime とかなり近い環境で開発しつつ、Vite の HMR で更新を素早く確認できます。UI を細かく触る章と相性がとてもよく、ボタン文言やカード配置を何度も微調整しやすいのが嬉しいポイントです ⚡ (Cloudflare Docs)


Workers AI を見越すなら、今のうちにこう設計すると強い 🤖🌈

Cloudflare Workers AI は、Workers から呼べるサーバーレス AI 実行基盤で、現在のモデル一覧には多言語向けの glm-4.7-flash、大きな文脈長を持つ kimi-k2.5gemma-4-26b-a4b-it などの新しい選択肢も並んでいます。つまり、将来 AI をつなぐ前提で UI を作るなら、日本語テキスト入力、トーン選択、結果表示、履歴カードという構成はかなり相性が良いです。 (Cloudflare Docs)

この章ではまだ AI を呼ばなくて大丈夫です 🙆 でも「AI を置く場所」を先にデザインしておくことで、後の章で Cloudflare の AI サービスを自然に組み込めます。これは 2026 年の Cloudflare 学習ではかなり実践的な進め方です。 (Cloudflare Docs)


GitHub Copilot をどう使うといい? 🧑‍🚀✨

VS Code では Chat ビューを Windows なら Ctrl+Alt+I で開けて、Agent を選ぶと、ワークスペースを見ながら変更提案や実装支援を進めやすくなっています。さらに Agent ではツールも扱え、必要に応じて web 取得やコード探索のような支援も使えます。UI 作りの章では、丸投げよりも「狙いを短く伝えて、修正候補を出してもらう」使い方がかなり相性いいです。 (Visual Studio Code)

たとえばこんなお願いが使いやすいです。

App.tsx の見た目は変えすぎずに、
- textarea の下に入力文字数を表示
- result-card をもう少し読みやすく
- ボタンの disabled 状態を分かりやすく
の3点だけ直してください

あるいは、

この React コンポーネントで state が多すぎないか確認して、
初心者向けに保守しやすい形へ軽く整理してください

みたいな頼み方もおすすめです 🌟


初学者がハマりやすいポイント 😵‍💫➡️😌

1. 入力しても画面に反映されない value={prompt} だけ書いて onChange を忘れると起きやすいです。制御された入力では valueonChange はセットと覚えると安心です。 (React)

2. 配列表示で warning が出る map() で並べるときに key が必要です。今回は id を使っています。React は一覧の差分を追うために key を重視します。 (React)

3. state を増やしすぎて混乱する isEmptyhasTextcanSubmitshowResult のように増やしすぎると、あとで矛盾しやすいです。prompt.trim()result から計算できるものは、なるべくその場で判断するのがラクです。 (React)

4. コンポーネントを分けるタイミングが分からない 最初は App.tsx にまとめて大丈夫です。 ただし UI が大きくなってきたら、次のように分けると見通しが上がります。

  • PromptForm
  • ResultCard
  • HistoryList

React は再利用できる UI 部品を作るのが得意なので、画面が育ってから分ければ OK です。 (Cloudflare Docs)


余裕があればやってみたい小課題 🏃‍♂️💨

  1. 文字数カウンターを付ける
  2. 入力が100文字を超えたら注意文を出す
  3. トーンごとにカードの見出し色を変える
  4. 履歴カードに「再利用」ボタンを付ける
  5. Enter 送信ではなく、あえてボタン送信だけにして挙動を確認する

このあたりは全部、次章以降の API 接続や AI 接続の前にやっておくとかなり力になります ✨


この章のまとめ 🌸

この章の本質は、「React で見た目を作る」ではなく、Cloudflare アプリの前面に立つ UI を、あとでつなぎやすい形で整えることです。Cloudflare の公式 React 導線は、React SPA と Workers API を自然に組み合わせる構成になっているので、今ここで入力欄・ボタン・結果エリア・履歴表示を整えておくのは、とても実戦的です。 (Cloudflare Docs)

次の章では、この handlePreview() の仮処理を本物の Worker 呼び出しへ置き換えて、いよいよ「React の見た目」と「Cloudflare の処理」がつながります 🔗☁️✨

必要なら続けて、この流れそのままで 第7章 も同じ文体・同じ粒度で作れます。