Old-School Front-End · Frameworks / 2026 ・ No.04

いにしえのフロントエンド入門 フレームワーク編

第4回 React の最初の一歩 ── ひな形、JSX、コンポーネントと props

公開

ブラウザの画面。React で書いた経費精算システムの「新規申請」。件名に「大阪出張(10/14〜15)」、支払い方法は立て替え。明細 3 件:新幹線 東京→大阪 14720 円、ホテル 1泊 12800 円、取引先との昼食 100 ドル。合計 42,643 円と、領収書が必要というお知らせ
フロントエンド編の申請フォームを、React のコンポーネントで書きなおしはじめました(まだ行の追加・削除は動きません)(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)

歴史の3回を終えて、いよいよ React で書きはじめます。題材はフロントエンド編の申請フォームです。
第4回は、Vite でひな形を作り、画面を部品(コンポーネント)に分け、明細を props で渡すところまで。行の追加や削除は、まだ動きません(それは第5回の「状態」の仕事です)。そのかわり、JSX の正体、よくあるつまずき、保存したら画面が変わる仕組み、開発用と本番用のちがいを測ります。

一

ひな形を作る

新井やっと React です! まず何を入れればいいですか?

古井Node.js だけでいい。あとはコマンド3つ。

全15回の路線図(いまここ:第4回)

1 なぜ FW 2 jQuery→MVC 3 React 以後 4 React の一歩 5 状態と再描画 6 フォーム 7 データ取得 8 再描画のコスト 9 状態の置き場所 10 画面遷移 11 楽観的更新 12 Server Comp. 13 キャッシュ 14 テスト 15 公開と選び方 いまここ なぜと歴史 React の基本 速さと置き場所 画面とサーバー Next.js 仕上げ

React そのものは「画面の形を決める」道具で、ファイルをまとめたり開発用のサーバーを立てたりはしません。そこは Vite に任せます。第3回で Vue・Svelte・Solid をまとめたのと同じ道具です。

端末の画面。npm create vite@latest keihi-react -- --template react --no-interactive で create-vite v9.3.3 がひな形を作り、npm install で 24 個の部品が入り、npm run dev で VITE v8.3.5 が 324 ms で起動して http://localhost:5180/ を表示
ひな形を作り、部品を入れ、開発用のサーバーを立てるまで。コマンド3つです(npm install は2回目の 4 秒)(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)
# npm create vite@latest keihi-react -- --template react で作られたもの(create-vite 9.3.3)
  .gitignore                        
  .oxlintrc.json                    コードの点検(oxlint)の設定
  README.md                         
  index.html                        入口の HTML(<div id="root"> だけ)
  package.json                      使う部品と、dev・build のコマンド
  vite.config.js                    Vite の設定(React の部品を足すだけ)
  public/                           そのまま配るファイル
  src/main.jsx                      画面を #root に描きはじめる
  src/App.jsx                       いちばん外側のコンポーネント
  src/App.css, index.css, assets/   見本の見た目と画像(今回は消した)
# 作るのに 1.3 秒。npm install は 25 個の部品で 62 MB、17.5 秒(2回目は 4 秒)。npm run dev は約 0.3 秒で起動

入口の index.html には <div id="root"> が1つあるだけで、画面の中身はすべて src/ の JavaScript が描きます。フロントエンド編では HTML を先に書き、JavaScript があとから書きかえました。ここでは順番が逆です。

二

画面を部品に分ける

申請フォームを、6つの部品に分けました。部品は関数で、名前は大文字で始めます。

部品の木と、props の流れ

App 画面ぜんたい AboutSection 件名・支払い方法 ItemsSection 明細のカード ItemTable 表 TotalLine 合計 ItemRow ×3 1行ぶん rows, rate rows rows, rate row, n 見本の明細(sample.js)は App から入る props は上から下へだけ流れる。子は受け取って使うだけで、書きかえない 合計は TotalLine が rows から毎回計算する。どこにも保存しない
# src/sample.js
// 第4回:まだ「状態」を使わないので、明細は決まった見本を出す(第5回で、足したり消したりできるようにする)
import { T } from './text.js';

export const RATE = 151.23; // 1ドル何円か(第7回でサーバーから取る)

export const SAMPLE_ROWS = [
  { id: 1, date: '2026-10-14', category: 'transport', item: T.sample[0], amount: '14720', currency: 'JPY' },
  { id: 2, date: '2026-10-14', category: 'hotel', item: T.sample[1], amount: '12800', currency: 'JPY' },
  { id: 3, date: '2026-10-15', category: 'meeting', item: T.sample[2], amount: '100', currency: 'USD' },
];

# src/App.jsx(main の中)
        <form>
          <AboutSection />
          <ItemsSection rows={SAMPLE_ROWS} rate={RATE} />
          …
        </form>

<ItemsSection rows={SAMPLE_ROWS} rate={RATE} /> は、HTML の属性のように見えますが、関数に引数を渡しています。この引数を props と呼びます。

# src/components/ItemsSection.jsx
import { T } from '../text.js';
import ItemTable from './ItemTable.jsx';
import TotalLine from './TotalLine.jsx';

/** 明細のカード:表と合計を並べるだけ。受け取った rows を、子にそのまま渡す */
export default function ItemsSection({ rows, rate }) {
  return (
    <div className="card mb-4">
      <fieldset className="card-body">
        <legend className="h6 section-title">{T.items} <span id="count" className="badge text-bg-secondary">{T.count(rows.length)}</span></legend>
        <ItemTable rows={rows} />
        <TotalLine rows={rows} rate={rate} />
      </fieldset>
    </div>
  );
}

# src/components/ItemTable.jsx(tbody の中)
        <tbody id="items">
          {rows.map((row, i) => <ItemRow key={row.id} row={row} n={i + 1} />)}
        </tbody>

ItemsSection は受け取った rows を、表(ItemTable)と合計(TotalLine)にそのまま渡すだけ。ItemTable は rows.map(…) で1行ずつ ItemRow を作ります。値は上から下へだけ流れ、下の部品が上の値を書きかえることはありません。第3回の「片方向」は、この形のことです。

三

明細の1行

import { T } from '../text.js';

/** 明細の1行。親から受け取った row(props)を、そのまま表の1行にする */
export default function ItemRow({ row, n }) {
  return (
    <tr>
      <td><input type="date" className="form-control form-control-sm" name="date" defaultValue={row.date} aria-label={T.rowLabel(n, T.date)} /></td>
      <td>
        <select className="form-select form-select-sm" name="category" defaultValue={row.category} aria-label={T.rowLabel(n, T.category)}>
          <option value="">{T.choose}</option>
          {Object.entries(T.categories).map(([value, label]) => <option key={value} value={value}>{label}</option>)}
        </select>
      </td>
      <td><input type="text" className="form-control form-control-sm" name="item" defaultValue={row.item} aria-label={T.rowLabel(n, T.item)} /></td>
      <td><input type="text" className="form-control form-control-sm text-end" name="amount" defaultValue={row.amount} inputMode="numeric" autoComplete="off" aria-label={T.rowLabel(n, T.amount)} /></td>
      <td>
        <select className="form-select form-select-sm" name="currency" defaultValue={row.currency} aria-label={T.rowLabel(n, T.currency)}>
          <option value="JPY">{T.jpy}</option>
          <option value="USD">{T.usd}</option>
        </select>
      </td>
      <td><button type="button" className="btn btn-outline-danger btn-sm" aria-label={T.rowRemove(n)}>{T.remove}</button></td>
    </tr>
  );
}

見た目は HTML ですが、ちがいがいくつかあります。class ではなく className、for ではなく htmlFor、inputmode は inputMode。{ } の中には JavaScript の式が書けて、{row.item} なら row の内容が入ります。

入力欄は defaultValue で最初の値だけ入れました。React に「この欄の値」を任せるのは第5回からで、いまは打った文字はブラウザがそのまま持っています。

四

JSX の正体

ブラウザは JSX を読めません。Vite がブラウザに渡す前に、ふつうの JavaScript に変えています。本番用のビルドを、縮めない設定で出してみました。

# npm run build(縮めない設定)で出てきた ItemRow。JSX は jsx()・jsxs() の呼び出しになっている
function ItemRow({ row, n }) {
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("tr", { children: [
    /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
      type: "date",
      className: "form-control form-control-sm",
      name: "date",
      defaultValue: row.date,
      "aria-label": T.rowLabel(n, T.date)
    }) }),
    /* @__PURE__ */ (0, import_jsx_runtime.jsx)("td", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", {
      className: "form-select form-select-sm",
      name: "category",
    …
  ] });
}

<tr>…</tr> は jsxs("tr", { children: [...] }) という関数の呼び出しになり、属性はオブジェクトのキーになりました。ここから、JSX の決まりごとの理由が見えてきます。

  • className ── オブジェクトのキーは JavaScript の名前で書く(DOM でも element.className)
  • 大文字で始める ── 小文字は "tr" のような HTML の名前(文字)に、大文字は ItemRow という関数になる
  • 1つにまとめて返す ── 関数は値を1つしか返せない。並べたいときは <>…</>(Fragment)で包む
五

合計は、計算するだけ

import { T } from '../text.js';
import { calcTotal, needsReceipt } from '../money.js';

/** 合計と、領収書のお知らせ。合計は props から毎回計算する(どこにも保存しない) */
export default function TotalLine({ rows, rate }) {
  const total = calcTotal(rows, rate);
  return (
    <>
      <div className="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-3">
        <button type="button" className="btn btn-outline-primary btn-sm" id="add-row">{T.add}</button>
        <p className="mb-0 fs-5">{T.total}<output id="total" className="fw-bold">{T.yen(total)}</output></p>
      </div>
      {needsReceipt(total) && <p className="alert alert-warning py-2 mt-3 mb-0" id="receipt-note">{T.receipt}</p>}
      <p className="small text-body-secondary mt-2 mb-0" id="rate-note">{T.rate(rate)}</p>
    </>
  );
}

合計は rows から毎回計算して出すだけで、どこにも保存しません。フロントエンド編では、合計を画面の <output> に書きこみ、行が変わるたびに書きなおしていました(第1回で、そこがずれました)。ここでは「rows が決まれば合計も決まる」ので、ずれる場所がありません。

領収書のお知らせは {needsReceipt(total) && <p>…</p>}。条件が false なら何も出ません。hidden を付けたり外したりする代わりに、出すか出さないかを書きます。計算の関数(money.js)は、フロントエンド編の app.js に import を付けただけです。

六

わざとつまずく

新井HTML とほとんど同じですね。これなら間違えません。

古井じゃあ、みんながやる間違いを先に全部やっておこう。

# 開発用のサーバー(npm run dev)で、つまずきを1つずつ起こした。コンソールに出たものと、画面
class・for をそのまま
  Invalid DOM property `class`. Did you mean `className`?(for も同じ)
  → 画面はそのまま出る(警告だけ)
部品名を小文字(itemRow)
  <itemRow /> is using incorrect casing. … start its name with an uppercase letter.
  → <itemrow></itemrow> という知らない要素になり、行が出ない
value だけで onChange なし
  You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field.
  → 「0」を打っても 14720 のまま
map で key なし
  Each child in a list should have a unique "key" prop.
  → いまは出る(第5回で、ずれる例を測る)
props を書きかえた
  Cannot assign to read only property 'n' of object
  → 画面が真っ白(エラーで止まる)
内容に <img onerror=…>
  (メッセージなし)
  → { } では文字のまま。dangerouslySetInnerHTML では img ができて onerror が動いた
2つの要素を並べて返した
  [PARSE_ERROR] Adjacent JSX elements must be wrapped in an enclosing tag.
  → 変換で止まり、Vite のエラー画面が出る
ブラウザの画面。Vite のエラー画面。[plugin:vite:oxc] Transform failed with 1 error: [PARSE_ERROR] Adjacent JSX elements must be wrapped in an enclosing tag. src/lab/LabBroken.jsx の 5 行目の <p>明細を入力してください。</p> を指し、Help: Did you want a JSX fragment <>...</>? と出ている
1つの部品から2つの要素を並べて返すと、ブラウザに届く前の変換で止まり、Vite がエラー画面を出しました(筆者がクラウド上の Linux で、2026年11月1日に撮影。ウインドウの上の帯は説明のために描き足したもの)

開発用のサーバーの React は、まちがいを見つけるとコンソールに理由を書いてくれます(className の提案まで)。ただし画面は止まらないことが多く、見ていなければ気づきません。小文字の部品名は、行が1つも出ないのに、エラーではなく警告です。

XSS は、{ } で入れた文字は必ず文字のまま出ます。フロントエンド編 第8回で textContent を選んだのと同じ守りが、最初から付いています。名前のとおり危ない dangerouslySetInnerHTML を使うと、onerror が動きました。

七

保存すると、すぐ変わる

開発用のサーバーは、ファイルを保存すると、その部品だけを差しかえます(HMR)。件名に「大阪出張」と打ってから、いろいろなファイルを書きかえてみました。

  書きかえたファイルどうなったか打った件名
  components/TotalLine.jsx        その部品だけ差しかえ(50 ms)残った
  components/AboutSection.jsx     その部品だけ差しかえ(51 ms)残った
  sample.js                       App.jsx を差しかえ残った
  text.js                         ページを読みこみなおし消えた
  main.jsx                        ページを読みこみなおし消えた
# 件名に「大阪出張」と打ってから、ファイルを書きかえた。時間は5回の中央値(ファイルを保存してから画面が変わるまで)
# text.js は main.jsx からも読まれているので、差しかえの範囲が main.jsx まで広がり、読みこみなおしになった

部品のファイルなら 50 ms ほどで変わり、打った件名も明細の内容も残りました。ところが、画面の文字をまとめた text.js を書きかえると、ページごと読みこみなおして件名が消えました。text.js は main.jsx からも読まれていて、main.jsx は部品ではないので、差しかえられないからです。部品だけを書いたファイルにしておくと、HMR がよく効きます。

八

開発用と本番用

同じ画面を、npm run dev と、npm run build したものを npm run preview で配ったものとで、開いてみました。

                            開発用(npm run dev)本番用(npm run build)
  読みこんだファイル22                      4
  そのうち JavaScript20                      1
  届いた大きさ3.8 MB                  451 KB
  合計が出るまで778 ms                  344 ms
# 開発用でいちばん大きいのは react-dom_client.js(3,132,085 B)。警告を出す仕組みと、元のソースへの対応表が入っている
# 開発用では CSS も JavaScript のファイルとして届く(bootstrap.min.css をふくむ)。時間は5回の中央値

開発用と本番用

同じ申請フォームを、開発用と本番用で開いた(実測、時間は5回の中央値) ファイルの数 開発用 22 本番用 4 届いた大きさ 開発用 3,909 KB 本番用 451 KB 合計が出るまで 開発用 778 ms 本番用 344 ms 開発用は、ファイルを1つずつ配り、警告と元のソースへの対応表を入れて大きい 本番用は、1つの JavaScript にまとめて縮める。人に見せるのは、いつも本番用

開発用は、ファイルを1つずつ配り(HMR で1つだけ差しかえるため)、六の警告を出す仕組みと、元のソースへの対応表まで入っているので 3.8 MB ありました。本番用は1つにまとめて縮めて 451 KB。速さを測るのも、人に見せるのも、本番用です。

九

自分のコードは何 KB か

                                        大きさgzip
  React だけ(中身が <p> 1つ)218,795 B     67,378 B
  この回の申請フォーム229,019 B     71,093 B
    → 自分のコードの分10,224 B      3,715 B
  jQuery 版(フロントエンド編 第14回)95,275 B      35,125 B
# jQuery 版は jquery・app・auth・validate・form-jq の5つ(送信や入力チェックまで動く)。この回の src は空行を除いて 228 行

中身が <p> 1つだけの React でも 219 KB あり、この回の申請フォームとの差、つまり自分で書いた分は 10 KB でした。React は「最初に払う分」が大きく、そのあとは部品を足しても少しずつしか増えません。jQuery 版は送信や入力チェックまで動いて 95 KB。いまの時点では jQuery 版のほうが軽く、できることも多いのが正直なところです。

十

自分の PC で

# ひな形を作って、入れて、開発用のサーバーを立てる
npm create vite@latest keihi-react -- --template react
cd keihi-react
npm install
npm run dev            # http://localhost:5173/ を開く
# 本番用を作って、手元で配ってみる
npm run build
npm run preview        # http://localhost:4173/

ブラウザに React Developer Tools を入れると、開発者ツールの Components タブで、部品の木と、それぞれが受け取った props が見られます。二の図と見比べてください。なお、ひな形の main.jsx にある <StrictMode> は、開発用でだけ部品を2回動かして間違いを見つける仕組みです。第7回で、その意味を測ります。

結 — 一枚まとめと、次回

  1. ひな形はコマンド3つ。React は画面の形を決め、まとめるのと配るのは Vite。
  2. 画面は部品(関数)に分ける。値は props で上から下へだけ渡し、子は書きかえない。
  3. JSX は jsx() の呼び出し。className・大文字・1つにまとめる、はそこから来る。
  4. 合計は保存せず、毎回計算するだけ。お知らせは「出すか出さないか」を書く。
  5. 開発用は警告つきで 3.8 MB、本番用は 451 KB。自分のコードは 10 KB。

新井画面はできました。……「行を追加」を押しても、何も起きないんですけど。

古井押したら何が変わるのか、まだ React に教えていないからね。

古井次回は状態と再描画。useState で行を足したり消したりして、key をまちがえると入力がずれるところまで測る。

参考

記録は、筆者がクラウド上の Linux で、2026年11月1日に取ったものです。create-vite 9.3.3 の react のひな形(Vite 8.3.5、@vitejs/plugin-react 6.1.2、React 19.3.1、oxlint)に、Bootstrap 5.3.8 を足しました。画面は Playwright で動かした Chromium 153.0.8010.12 で開き、コンソールの内容、ファイルの数と大きさ、時間を取りました。HMR の時間はファイルを保存してから画面が変わるまで、読みこみの時間はナビゲーションの開始から合計が出るまでで、どちらも5回の中央値です。npm install の時間は通信の具合で変わります。

いにしえのフロントエンド入門 フレームワーク編 第4回 — React の最初の一歩 / 2026.11

← 記事一覧へ