1.1 Reactとは何か
Reactは、Webおよびネイティブ環境のユーザーインターフェースを構築するためのJavaScriptライブラリである。
Reactの説明では「Virtual DOM」が強調されることがあるが、Reactの本質をVirtual DOMだけで説明するのは不十分である。重要なのは次の考え方である。
UIを直接変更するのではなく、現在のstateに対してUIがどう見えるべきかを記述する。
命令的なDOM操作では、開発者が変化の手順を記述する。
const button = document.querySelector("#counter");
const label = document.querySelector("#count");
let count = 0;
button.addEventListener("click", () => {
count += 1;
label.textContent = String(count);
button.disabled = count >= 10;
});
Reactでは、状態と表示の対応関係を記述する。
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>回数: {count}</p>
<button
type="button"
disabled={count >= 10}
onClick={() => setCount(current => current + 1)}
>
加算
</button>
</section>
);
}
このコードでは「pを書き換える」「ボタンの属性を変更する」という手順を直接書いていない。
countが変化したときに表示されるべきUIを定義している。
1.2 Reactが解決する問題
画面が小さいうちは、DOMを直接操作しても管理できる。しかし、次のような状態が増えると複雑になる。
- ログイン中か
- データを読み込み中か
- 取得に失敗したか
- 入力内容が妥当か
- 保存中か
- 保存に成功したか
- モーダルが開いているか
- 選択中のタブはどれか
- ユーザー権限は何か
命令的な実装では、「状態Aから状態Bに変わるとき、どのDOMをどの順序で変更するか」という遷移の組み合わせが増える。
Reactは、各状態について最終的なUIの形を記述し、状態変化からDOM更新までをReactに委ねる。
function Profile({ status, user, error }) {
if (status === "loading") {
return <p>読み込み中です。</p>;
}
if (status === "error") {
return <p role="alert">{error.message}</p>;
}
if (!user) {
return <p>ユーザーが見つかりません。</p>;
}
return <h1>{user.name}</h1>;
}
1.3 宣言的UI
宣言的UIとは、どう変更するかではなく、何を表示するかを記述する方式である。
命令的
if (isLoading) {
spinner.hidden = false;
content.hidden = true;
errorBox.hidden = true;
} else if (error) {
spinner.hidden = true;
content.hidden = true;
errorBox.hidden = false;
} else {
spinner.hidden = true;
content.hidden = false;
errorBox.hidden = true;
}
宣言的
function Screen({ isLoading, error, children }) {
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return children;
}
宣言的であることには次の利点がある。
- 状態と表示の対応を読み取りやすい
- DOM更新の漏れを減らせる
- コンポーネント単位でテストできる
- 並行レンダリングやサーバーレンダリングと整合しやすい
- Compilerがコードを分析しやすい
1.4 コンポーネント
コンポーネントは、UIの一部を表すJavaScript関数である。
function Greeting() {
return <h1>こんにちは</h1>;
}
使用時はHTML要素のように記述する。
function App() {
return (
<main>
<Greeting />
<Greeting />
</main>
);
}
コンポーネント名は大文字から始める。
// Reactコンポーネント
function UserCard() {
return <article />;
}
// 組み込みHTML要素として解釈される
function userCard() {
return <article />;
}
コンポーネントの責務
良いコンポーネントは、通常、次のいずれかのまとまりを表す。
- 画面上の意味のある領域
- 再利用されるUI
- 独立した状態を持つUI
- 複雑な処理を隠蔽する境界
- デザインシステム上の部品
分割しすぎると追跡が難しくなり、分割しなさすぎると変更影響が広がる。
「何行になったら分割する」という機械的基準ではなく、変更理由が同じかを基準にする。
1.5 JSX
JSXは、JavaScript内でUI構造を記述する構文拡張である。
const element = <h1>Hello</h1>;
ブラウザは通常、JSXをそのまま実行しない。ビルド時にJavaScriptへ変換される。
概念的には、次のような要素生成処理になる。
const element = jsx("h1", { children: "Hello" });
JSXはHTMLに似ているが、HTMLそのものではない。
function Avatar({ imageUrl, name }) {
return (
<img
className="avatar"
src={imageUrl}
alt={`${name}のプロフィール画像`}
/>
);
}
主な違いは次のとおりである。
classではなくclassName- JavaScript式は
{} - タグを閉じる
- 複数要素は単一の親要素かFragmentで囲む
- 属性名の多くはcamelCase
- イベントには関数を渡す
1.6 props
propsは、親コンポーネントから子コンポーネントへ渡す入力である。
type UserCardProps = {
name: string;
role: string;
};
function UserCard({ name, role }: UserCardProps) {
return (
<article>
<h2>{name}</h2>
<p>{role}</p>
</article>
);
}
function App() {
return <UserCard name="中川" role="Developer" />;
}
propsは読み取り専用として扱う。
function BadExample(props: { name: string }) {
// 禁止: 受け取ったpropsを変更する
// props.name = "別名";
return <p>{props.name}</p>;
}
propsを変更したい場合は、親が新しい値を渡すか、コンポーネント内部のstateとして管理する。
1.7 childrenと合成
コンポーネントの開始タグと終了タグの間に置かれた内容は、childrenとして渡される。
import type { ReactNode } from "react";
type PanelProps = {
title: string;
children: ReactNode;
};
function Panel({ title, children }: PanelProps) {
return (
<section>
<h2>{title}</h2>
<div>{children}</div>
</section>
);
}
function App() {
return (
<Panel title="お知らせ">
<p>システム更新を実施します。</p>
</Panel>
);
}
継承よりも合成を使うのがReactの基本である。
Panelは内部の具体的な内容を知らず、配置だけを担当する。
1.8 state
stateは、時間とともに変化し、画面に影響を与えるコンポーネントの記憶である。
import { useState } from "react";
function Toggle() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button type="button" onClick={() => setIsOpen(open => !open)}>
{isOpen ? "閉じる" : "開く"}
</button>
{isOpen && <p>詳細情報</p>}
</div>
);
}
useState(false)は次の2つを返す。
- 現在のstate
- stateを更新する関数
const [isOpen, setIsOpen] = useState(false);
state更新は、変数への代入ではない。
// 表示更新を起こさない
isOpen = true;
// Reactへ更新を依頼する
setIsOpen(true);
1.9 stateはスナップショットである
レンダリング中に参照するstateは、そのレンダリング時点のスナップショットである。
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
}
return <button onClick={handleClick}>{count}</button>;
}
この例では、3回とも同じレンダリングのcountを参照するため、通常は1だけ増える。
前回値を基準に連続更新するときは、更新関数を使う。
function handleClick() {
setCount(current => current + 1);
setCount(current => current + 1);
setCount(current => current + 1);
}
この場合は3増える。
1.10 イベント
Reactではイベントハンドラーに関数を渡す。
function SaveButton() {
function handleClick() {
console.log("保存");
}
return (
<button type="button" onClick={handleClick}>
保存
</button>
);
}
関数を呼び出した結果を渡してはいけない。
// 正しい
<button onClick={handleClick}>保存</button>
// レンダリング中に即座に実行される
<button onClick={handleClick()}>保存</button>
引数が必要なら関数で包む。
<button onClick={() => deleteItem(item.id)}>
削除
</button>
1.11 レンダリング
Reactの画面更新は、概念的に次の段階で進む。
- Trigger
- 初回表示
- state更新
- 親の再レンダリング
- Context値の変化
- Render
- コンポーネント関数を呼び出す
- 次に必要なUIを計算する
- Commit
- 必要な変更をDOMへ反映する
- Browser paint
- ブラウザが画面を描画する
レンダリングとは、必ずしもDOMを書き換えることではない。
コンポーネント関数が再実行されても、出力に差がなければDOM更新は発生しない場合がある。
1.12 純粋なレンダリング
コンポーネントは、同じ入力に対して同じJSXを返す純粋関数として書く。
function Price({ amount }: { amount: number }) {
return <span>{amount.toLocaleString("ja-JP")}円</span>;
}
レンダリング中に外部の値を変更してはいけない。
let nextId = 0;
function BadListItem() {
// レンダリング回数により結果が変わる
const id = nextId++;
return <li id={`item-${id}`}>項目</li>;
}
副作用は、イベントハンドラー、Effect、サーバー処理など適切な場所へ移す。
純粋性が重要な理由は次のとおり。
- Reactはレンダリングを中断・再開・再試行できる
- Strict Modeでは問題検出のため複数回呼ばれることがある
- Server Componentsでも同じモデルを利用する
- React Compilerが安全に最適化できる
- テストが決定的になる
1.13 UIツリーとstateの同一性
stateはコンポーネント関数そのものではなく、レンダリングツリー上の位置に結び付く。
function App() {
const [show, setShow] = useState(true);
return (
<>
<button onClick={() => setShow(value => !value)}>切り替え</button>
{show && <Counter />}
</>
);
}
Counterがツリーから削除されると、そのstateも破棄される。再度表示すると初期化される。
同じ位置・同じ型の要素は、原則としてstateを保持する。
function App({ isCompany }: { isCompany: boolean }) {
return (
<main>
{isCompany ? (
<Form mode="company" />
) : (
<Form mode="individual" />
)}
</main>
);
}
この場合、同じ位置に同じFormがあるため、stateが保持される可能性がある。
別のフォームとして初期化したい場合はkeyを使う。
<Form key={isCompany ? "company" : "individual"} mode={...} />
1.14 key
keyは一覧要素の同一性をReactへ伝える。
function UserList({ users }: { users: User[] }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
良いkeyは次の条件を満たす。
- 兄弟要素の中で一意
- データに由来する
- 並べ替えや追加で変化しない
- レンダリングのたびに生成しない
配列indexは、並べ替え・挿入・削除がない静的一覧以外では避ける。
// 問題が起こりやすい
items.map((item, index) => <Row key={index} item={item} />)
// 推奨
items.map(item => <Row key={item.id} item={item} />)
keyは子コンポーネントのpropsとして渡されない。
function Row({ id, name }: { id: string; name: string }) {
return <li>{name}</li>;
}
<Row key={user.id} id={user.id} name={user.name} />
1.15 単方向データフロー
Reactでは、データは基本的に親から子へ流れる。
function Parent() {
const [name, setName] = useState("");
return (
<NameInput
value={name}
onChange={setName}
/>
);
}
function NameInput({
value,
onChange,
}: {
value: string;
onChange: (value: string) => void;
}) {
return (
<input
value={value}
onChange={event => onChange(event.target.value)}
/>
);
}
- 値は
valueとして下へ渡す - 更新要求はコールバックとして上へ伝える
- stateの所有者は親である
複数コンポーネントが同じstateを必要とする場合、共通の親へstateを引き上げる。
1.16 制御コンポーネント
フォーム要素の値をReact stateで管理する方式を制御コンポーネントと呼ぶ。
function SearchForm() {
const [query, setQuery] = useState("");
return (
<form>
<label>
キーワード
<input
value={query}
onChange={event => setQuery(event.target.value)}
/>
</label>
<p>検索語: {query}</p>
</form>
);
}
React stateを使わずDOM自身に保持させる方式は非制御コンポーネントである。
どちらが常に優れているわけではないが、入力値に応じて表示や検証を即時更新する場合は制御方式が扱いやすい。
1.17 条件付きレンダリング
JavaScriptの条件分岐を使ってUIを切り替える。
function LoginStatus({ user }: { user: User | null }) {
if (!user) {
return <LoginButton />;
}
return <p>{user.name}としてログイン中</p>;
}
三項演算子は、JSXの一部分を切り替える場合に向く。
<p>{isOnline ? "オンライン" : "オフライン"}</p>
論理ANDは、条件が真のときだけ表示する場合に使う。
{error && <p role="alert">{error.message}</p>}
数値を条件に使うと0が表示されることがある。
// items.lengthが0なら0が描画される
{items.length && <List items={items} />}
// 明示的なbooleanにする
{items.length > 0 && <List items={items} />}
1.18 一覧レンダリング
配列からJSXの配列を生成する。
function ProductList({ products }: { products: Product[] }) {
return (
<ul>
{products.map(product => (
<li key={product.id}>
{product.name}: {product.price.toLocaleString("ja-JP")}円
</li>
))}
</ul>
);
}
絞り込みと変換を組み合わせられる。
const availableProducts = products
.filter(product => product.stock > 0)
.map(product => (
<ProductRow key={product.id} product={product} />
));
map内に複雑な処理が増えたら、子コンポーネントまたは事前計算へ分離する。
1.19 イミュータビリティ
Reactのstateは、既存のオブジェクトや配列を直接変更せず、新しい値として更新する。
type Profile = {
name: string;
city: string;
};
const [profile, setProfile] = useState<Profile>({
name: "中川",
city: "東京",
});
setProfile(current => ({
...current,
city: "大阪",
}));
配列への追加:
setItems(current => [
...current,
newItem,
]);
削除:
setItems(current =>
current.filter(item => item.id !== targetId)
);
更新:
setItems(current =>
current.map(item =>
item.id === updated.id ? updated : item
)
);
直接変更すると、Reactが変更を認識できない、過去のスナップショットを壊す、Compilerやメモ化を妨げるなどの問題が生じる。
1.20 最小アプリケーション
import { useMemo, useState } from "react";
type Task = {
id: string;
title: string;
completed: boolean;
};
export default function App() {
const [title, setTitle] = useState("");
const [tasks, setTasks] = useState<Task[]>([]);
const remainingCount = useMemo(
() => tasks.filter(task => !task.completed).length,
[tasks]
);
function addTask() {
const normalizedTitle = title.trim();
if (!normalizedTitle) {
return;
}
setTasks(current => [
...current,
{
id: crypto.randomUUID(),
title: normalizedTitle,
completed: false,
},
]);
setTitle("");
}
function toggleTask(id: string) {
setTasks(current =>
current.map(task =>
task.id === id
? { ...task, completed: !task.completed }
: task
)
);
}
return (
<main>
<h1>タスク管理</h1>
<p>未完了: {remainingCount}件</p>
<div>
<input
aria-label="タスク名"
value={title}
onChange={event => setTitle(event.target.value)}
/>
<button type="button" onClick={addTask}>
追加
</button>
</div>
<ul>
{tasks.map(task => (
<li key={task.id}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
/>
{task.title}
</label>
</li>
))}
</ul>
</main>
);
}
このアプリにはReactの基礎が凝縮されている。
- コンポーネント
- JSX
- propsに相当する属性
- state
- イベント
- 制御入力
- 配列更新
- 条件ではなくstateから導出されるUI
- 一覧と
key - イミュータブル更新
- 派生値
なお、2026年のReact Compilerを利用する環境では、この程度の派生計算に手動のuseMemoが必要かは測定とCompilerの適用状況で判断する。ここではAPIの例として記載した。
1.21 第1章の要点
- Reactは状態からUIを導出するライブラリである
- UIはコンポーネントのツリーとして構築する
- JSXはUIをJavaScript内で表現する構文である
- propsは親から渡される読み取り専用の入力である
- stateは時間とともに変化するコンポーネントの記憶である
- state更新は次回レンダリングを要求する
- レンダリングは純粋でなければならない
- stateはツリー上の位置に結び付く
keyは要素の同一性を表す- データは原則として親から子へ流れる
- オブジェクトと配列はイミュータブルに更新する
- Reactの基礎はAPI暗記ではなく、レンダリングモデルの理解である