はじめに
Reactのコードを読んでいると ref や document や HTMLButtonElement といった言葉が出てきます。「なんとなくHTMLに関係するもの」という感覚はあるけど、正確に何を指しているのかわからない——そういう人向けに(かくいう私もそうですが)、Webページが表示される仕組みを自分なりに整理してみました。
1. Webページが表示されるまでの流れ
ブラウザがWebページを表示するとき、裏側では以下のことが起きています。
HTMLファイル(テキスト)
↓ ブラウザが読み込む
DOM(ツリー構造のオブジェクト)
↓ CSSと組み合わせて計算
レイアウト(各要素の位置・サイズを決定)
↓
画面に描画(ピクセルとして表示)
HTMLはただのテキストです。ブラウザはそれを読み込んで「DOM」というデータ構造に変換し、それをもとに画面を描画しています。
2. DOMとは何か
DOM(Document Object Model) とは、HTMLを「操作できるオブジェクトのツリー」として表現したものです。
たとえばこのHTMLがあるとします。
<body>
<div>
<button>送信</button>
<p>テキスト</p>
</div>
</body>
ブラウザはこれを以下のようなツリー構造として解釈します。
Window(ブラウザ全体)
└── Document(HTMLファイル全体)
└── <body>
└── <div>
├── <button>送信</button>
└── <p>テキスト</p>
このツリーの各パーツを「ノード(Node)」と呼びます。
DOMのノードの親子関係
DOMのノードには以下のような継承関係があります。
EventTarget ← イベント(クリック等)を受け取れる
└── Node ← 親子関係・追加・削除ができる
└── Element ← 画面に表示される要素
└── HTMLElement ← HTMLに特化した要素
├── HTMLButtonElement ← <button>
├── HTMLInputElement ← <input>
└── HTMLDivElement ← <div>
私たちが <button> と書いているのは、実は HTMLButtonElement というオブジェクトの「省略記法」です。このオブジェクトは disabled や onClick といったプロパティを持っています。
詳しくは HTML DOM API — MDN を参照してください。
3. JavaScriptからDOMを操作する
DOMはJavaScriptから操作できます。
// IDでHTML要素を取得する
const button = document.getElementById('myButton');
// プロパティを変更する → 画面が変わる
button.disabled = true; // ボタンを無効化
button.textContent = '送信中...'; // テキストを変更
document は「HTMLファイル全体を表すオブジェクト」です。window → document → 各要素、という親子関係になっています。
4. レンダリングとは何か
レンダリングとは「データを画面に描画すること」です。
DOMを変更するたびに、ブラウザは以下を再計算します。
DOMの変更
↓
レイアウトの再計算(各要素の位置・サイズ)
↓
画面の再描画
これはコストのかかる処理です。DOMを1回変更するたびにブラウザが再計算するため、変更が多いと画面がカクつきます。
5. 仮想DOMとは何か — なぜReactが生まれたか
2013年頃のWeb開発では、状態が変わるたびに直接DOMを操作するコードを書いていました。
// 昔のやり方(jQuery的なアプローチ)
function onSuccess() {
document.getElementById('button').disabled = true;
document.getElementById('button').textContent = '完了!';
document.getElementById('loading').style.display = 'none';
document.getElementById('success').style.display = 'block';
}
状態が複雑になるほどこのコードは増え、「どの状態のときにどのDOMをどう変えるか」を追うのが難しくなっていきました。
Reactはこの問題を「仮想DOM(Virtual DOM)」という仕組みで解決しました。
仮想DOMの仕組み
状態(state)が変わる
↓
Reactが新しい「仮想DOM」(メモリ上のツリー)を作る
↓
前の仮想DOMと比較して「差分」を計算する(diffing)
↓
差分だけを実際のDOMに反映する(最小限の変更)
// Reactのやり方
function Button() {
const [status, setStatus] = useState('idle');
// 状態を変えるだけ → Reactが差分を計算してDOMを更新してくれる
return (
<button disabled={status === 'pending'}>
{status === 'pending' ? '送信中...' : '送信する'}
</button>
);
}
私たちは「状態(state)がこのときはこう表示する」とだけ書けばよく、DOMの操作はReactが自動でやってくれます。
6. マウントとアンマウント
Reactにはコンポーネントのライフサイクルがあります。
マウント(Mount): コンポーネントが初めて画面に表示されること
ReactがコンポーネントのJSXを評価
↓
仮想DOMを生成
↓
実際のDOMに追加される(マウント)
↓
ユーザーに表示される
アンマウント(Unmount): コンポーネントが画面から取り除かれること
// タブを切り替えると UserCard がアンマウントされる
{activeTab === 'user' && <UserCard />}
7. useCallbackとは何か — 関数の再生成を防ぐ
useCallbackは、再レンダー間で関数定義をキャッシュ(メモ化)できるようにするReactフックです。
メモ化(キャッシュする)とは
メモ化とは、同じ結果を返す処理について初回のみ処理を実行記録しておき、値が必要となった2回目以降は前回の処理結果を計算することなく呼び出し値を得られるようにすることです。これにより、不要に生成される関数インスタンスの作成および再描画が減り、パフォーマンスの向上が期待できます。
⚠️ 注意:生成と実行の違い
- 「関数が 生成される こと」と「呼び出される(実行される)こと」は異なります。
- 「関数 をメモ化すること」と「関数の結果 をメモ化(=キャッシュ)すること」は異なります。
なぜ useCallback が必要なのか?
ReactはStateが変わるたびにコンポーネントを再レンダリングします。このとき、コンポーネント内で定義した関数も毎回新しく作り直されます。
function ProductPage({ productId }) {
// 再レンダリングのたびに handleSubmit が新しい関数として作られる
function handleSubmit(orderDetails) {
api.submit(productId, orderDetails);
}
// 子(ShippingForm)は、Propsの関数が「別物」になったと判断して再描画してしまう
return <ShippingForm onSubmit={handleSubmit} />;
}
通常これは問題になりませんが、子コンポーネントに関数を渡しているとき、**「関数が毎回新しい = 子コンポーネントも毎回再レンダリングされる」**という問題が起きます。
useCallback を使うと、依存配列の値が変わらない限り、同じ関数を使い回せます。
import { useCallback } from 'react';
function ProductPage({ productId }) {
// productId が変わらない限り、同じ関数を返す
const handleSubmit = useCallback((orderDetails) => {
api.submit(productId, orderDetails);
}, [productId]); // ← productId が依存配列
return <ShippingForm onSubmit={handleSubmit} />;
}
behave-uiのAsyncButton での使われ方
実際のライブラリでは、以下のように useCallback が活用されています。
// AsyncButton/index.tsx
const handleClick = useCallback(async () => {
await execute(onClick); // onClick が変わらない限り同じ関数
}, [execute, onClick]); // ← execute と onClick が依存配列
onClick(ユーザーが渡す非同期関数)が変わらない限り handleClick を再生成しません。これにより不要な再レンダリングを防いでいます。
詳しくはuseCallback — React公式 を参照してください。
過去に書いたbehave-uiに関する記事
8. refとは何か — DOMノードに直接アクセスする手段
Reactでは通常、状態(state)を変えることで画面を更新します。しかし場合によっては、DOMノードに直接アクセスしたいことがあります。
- フォーカスを当てたい
- スクロール位置を制御したい
- 子コンポーネントのDOM要素を操作したい
そのために使うのが ref です。
import { useRef } from 'react';
function SearchBox() {
const inputRef = useRef(null);
function handleClick() {
// DOMノードに直接アクセスして focus() を呼ぶ
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>検索欄にフォーカス</button>
</>
);
}
ref.current には、実際の HTMLInputElement(DOMノード)が入っています。
refとstateの違い
| state | ref | |
|---|---|---|
| 変更したとき | 再レンダリングが起きる | 再レンダリングしない |
| 用途 | 画面の表示を変えたいとき | DOMを直接操作したいとき |
| アクセス方法 |
state の値 |
ref.current |
詳しくは refでDOMを操作する — React公式 を参照してください。
9. forwardRefとは何か — 親から子のDOMにアクセスする
通常、ref は自分のコンポーネント内のDOMにしか使えません。
// 親コンポーネントから子の<button>のDOMにアクセスしたい
<MyButton ref={buttonRef} /> // ← 通常はこれができない
forwardRef を使うと、親から渡された ref を子コンポーネントの内側のDOMに繋げられます。
const MyButton = forwardRef((props, ref) => {
// ↑ 親から渡されたref
return <button ref={ref} {...props} />;
// ↑ 内側のDOMに接続
});
// 親からボタンのDOMに直接アクセスできるようになる
const buttonRef = useRef(null);
<MyButton ref={buttonRef} />
buttonRef.current.focus(); // HTMLButtonElementのメソッドを呼べる
10. behave-uiのAsyncButtonで全部つながる
OSS「behave-ui」の AsyncButton コンポーネントが、ここまで説明した概念をどう使っているかが見えてきます。
export const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>(
function AsyncButton({ onClick, loadingText, ... }, ref) {
// useAsyncState で idle/pending/success/error を管理
const { status, execute } = useAsyncState({ ... });
// useCallback で不要な再レンダリングを防ぐ
const handleClick = useCallback(async () => {
await execute(onClick);
}, [execute, onClick]);
return (
<button
ref={ref} // ← forwardRef: 親からDOMにアクセス可能に
data-status={status} // ← DevToolsで状態を確認できる
disabled={status === 'pending'} // ← pending中だけ無効化
aria-busy={status === 'pending'} // ← スクリーンリーダー向け
onClick={handleClick}
>
{/* 状態に応じて表示内容を切り替え */}
{status === 'pending' ? <><Spinner />{loadingText}</> : children}
</button>
);
}
);
| 使われている概念 | 役割 |
|---|---|
| DOM / HTMLButtonElement |
<button> タグの実体 |
| ref / forwardRef | 親コンポーネントがボタンのDOMを操作できるようにする |
| useCallback |
handleClick の不要な再生成を防ぐ |
| state(useAsyncState内) | idle/pending/success/errorの状態管理 |
disabled={status === 'pending'} |
pendingのときだけ無効化(常にtrueではない) |
11.【試験対策】レンダリング・マウント・コミットの違い
「レンダリング」という言葉は文脈によって意味が変わるため、試験や面接では正確に使い分けられると差がつきます。
React公式の定義(3つのフェーズ)
Reactの公式ドキュメントでは、画面が更新されるまでを以下の3つのフェーズに分けています。
① レンダリング(Rendering)
JSXを評価 → 仮想DOMを生成 → 前回と差分を計算
↓
② コミット(Committing)
差分を実際のDOMに反映する
↓
③ ブラウザの描画(Browser Paint)
DOMの変更 → レイアウト再計算 → 画面にピクセルを描く
React公式における「レンダリング」は①だけを指します。DOMへの反映(②)やブラウザの描画(③)は含みません。
マウントはどこに位置するか
マウントは②コミットフェーズの初回です。
初回(コンポーネントが初めて表示されるとき)
→ コミット = DOMへの「追加」= マウント
2回目以降(stateが変わって更新されるとき)
→ コミット = DOMの「差分書き換え」= 更新(Update)
一連の流れを整理すると
stateが変わる / 初回表示
↓
【レンダリング】JSXを評価して仮想DOMを生成・差分を計算
↓
【コミット】差分を実際のDOMに反映(初回はマウント)
↓
【ブラウザの描画】画面にピクセルとして表示
日常会話との違い
エンジニア同士の会話では「レンダリング」がこの一連の流れ全体を指すことも多く、厳密に使い分けている人は少ないです。ただし面接や試験では以下のように答えると正確です。
| 用語 | React公式の意味 |
|---|---|
| レンダリング | JSXを評価して仮想DOMを生成し、差分を計算するフェーズ |
| コミット | 計算した差分を実際のDOMに書き込むフェーズ |
| マウント | コミットフェーズの初回(DOMへの初めての追加) |
| ブラウザの描画 | コミット後にブラウザが画面に描画するフェーズ(Reactの管轄外) |
まとめ
| 概念 | 一言で言うと |
|---|---|
| HTML | テキストで書いた「画面の設計図」 |
| DOM | ブラウザがHTMLを変換した「操作可能なオブジェクトのツリー」 |
| ノード | DOMのツリーを構成する1つ1つのパーツ |
| HTMLButtonElement |
<button> タグに対応するDOMオブジェクト(disabled等を持つ) |
| レンダリング(React公式) | JSXを評価して仮想DOMを生成・差分を計算するフェーズ |
| コミット | 差分を実際のDOMに書き込むフェーズ |
| マウント | コミットフェーズの初回(DOMへの初めての追加) |
| 仮想DOM | Reactがメモリ上に持つ「差分計算用のDOMのコピー」 |
| useCallback | 関数を再レンダリングをまたいでキャッシュする仕組み |
| ref | DOMノードへの「直接参照」を持つオブジェクト |
| forwardRef | 親から子コンポーネントのDOMにrefを渡せるようにする仕組み |