第104章:練習:useLayoutEffect でDOMのサイズを測る
今回は、
「画面に 実際に 出た要素のサイズ(幅・高さ)を、Reactの中で知る」 ということをやってみます 🌟
useLayoutEffect の「タイミングの良さ」を、体で覚える回です 💪
1️⃣ ゴールのイメージ
こんな感じのコンポーネントを作ります 👇
- カラフルなボックス 📦 が1つ表示される
- そのボックスの「幅」と「高さ」を、実際の DOM から測って、画面に数字で表示する
- ウィンドウのサイズを変えると、数字も変わる(=ちゃんと測り直している)
イメージとしてはこんな流れです 👇
useLayoutEffect は
「DOMができた直後、画面に見えるタイミングの直前」 で動いてくれるので、レイアウトに関すること(サイズ計測など)と相性バツグンです ✨
2️⃣ 準備:ファイルを1つ追加しよう 📁
Vite + React + TS のプロジェクトがある前提で進めます。
src フォルダの中に、新しくこんなファイルを作ってください。
src/MeasureBox.tsx
ここに練習用コンポーネントを書いていきます ✍️
3️⃣ コード全体を先に見る 👀
まずは完成コードをドーンと載せます。 あとで分解して説明していくスタイルでいきますね 😊
// src/MeasureBox.tsx
import { useLayoutEffect, useRef, useState } from "react";
type BoxSize = {
width: number;
height: number;
};
export function MeasureBox() {
const boxRef = useRef<HTMLDivElement | null>(null);
const [size, setSize] = useState<BoxSize | null>(null);
useLayoutEffect(() => {
function updateSize() {
if (boxRef.current === null) {
return;
}
const rect = boxRef.current.getBoundingClientRect();
setSize({
width: Math.round(rect.width),
height: Math.round(rect.height),
});
}
updateSize();
window.addEventListener("resize", updateSize);
return () => {
window.removeEventListener("resize", updateSize);
};
}, []);
return (
<div
style={{
fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif",
padding: "24px",
}}
>
<h2 style={{ fontSize: "20px", marginBottom: "16px" }}>
useLayoutEffect で DOM のサイズを測る練習 📏
</h2>
<p style={{ marginBottom: "12px", lineHeight: 1.6 }}>
下のカラフルなボックスの「実際のサイズ」を、DOM から計測して表示しています。
ウィンドウの幅を変えてみてね! 👀
</p>
<div
ref={boxRef}
style={{
width: "60%",
minWidth: "260px",
padding: "24px",
background:
"linear-gradient(135deg, rgba(255,182,193,0.9), rgba(135,206,235,0.9))",
borderRadius: "16px",
boxShadow: "0 6px 20px rgba(0,0,0,0.12)",
color: "#333",
marginBottom: "16px",
transition: "width 0.2s ease",
}}
>
<strong>こんにちは 👋</strong>
<br />
<span>
このボックスの幅と高さを、
<code style={{ background: "rgba(255,255,255,0.7)", padding: "2px 4px", borderRadius: "4px" }}>
useLayoutEffect
</code>
で測っています。
</span>
</div>
<div
style={{
padding: "12px 16px",
borderRadius: "12px",
backgroundColor: "#f5f5f5",
display: "inline-block",
fontSize: "14px",
}}
>
{size === null ? (
<span>サイズを測定中です… ⏳</span>
) : (
<span>
現在のサイズ 👉 <strong>{size.width}px</strong>(幅) ×{" "}
<strong>{size.height}px</strong>(高さ)
</span>
)}
</div>
<p style={{ marginTop: "16px", fontSize: "13px", color: "#555" }}>
💡 ウィンドウの幅を変えると、ボックスの幅も変わるので、
表示されている数値も変わるはずです。
</p>
</div>
);
}
このコンポーネントを App.tsx から呼び出してあげます。
// src/App.tsx
import { MeasureBox } from "./MeasureBox";
function App() {
return <MeasureBox />;
}
export default App;
npm run dev で開いて、
ウィンドウの幅を動かしてみてください 🪟
数字が変われば成功です 🎉
4️⃣ ひとつずつ分解して理解しよう 🔍
4-1. ref で実際の DOM をつかまえる ✋
const boxRef = useRef<HTMLDivElement | null>(null);
-
useRefで「あとで DOM を入れてもらう箱」を作っています -
型は
HTMLDivElement | null- 最初はまだ何もないので
null - 描画が終わると、React がここに
<div>の実体を入れてくれます
- 最初はまだ何もないので
この boxRef を、サイズを測りたいボックスにつなげます。
<div
ref={boxRef}
style={{ /* いろいろスタイル */ }}
>
...
</div>
こうすると、描画後に boxRef.current の中身が
「実際の <div>(DOM要素)」になります ✨
4-2. サイズを State で持っておく 🧠
type BoxSize = {
width: number;
height: number;
};
const [size, setSize] = useState<BoxSize | null>(null);
- サイズは
{ width, height }のセットで扱いたいのでBoxSize型を作成 - 最初はまだ測っていないので
null - 測れたら
{ width: 〇〇, height: △△ }をセット
size === null のときは「測定中」と表示して、
そうでないときは数字を出すようにしています 👇
{size === null ? (
<span>サイズを測定中です… ⏳</span>
) : (
<span>
現在のサイズ 👉 <strong>{size.width}px</strong>(幅) ×{" "}
<strong>{size.height}px</strong>(高さ)
</span>
)}
UI的にも、こういう「まだ分からない状態」をちゃんと表現してあげると、 ユーザーにやさしいアプリになります 💗
4-3. 主役:useLayoutEffect の中身 🧪
useLayoutEffect(() => {
function updateSize() {
if (boxRef.current === null) {
return;
}
const rect = boxRef.current.getBoundingClientRect();
setSize({
width: Math.round(rect.width),
height: Math.round(rect.height),
});
}
updateSize();
window.addEventListener("resize", updateSize);
return () => {
window.removeEventListener("resize", updateSize);
};
}, []);
やっていることを順番に見ると… 👀
-
updateSize関数を定義boxRef.currentに DOM が入っているかチェックgetBoundingClientRect()でサイズを取得setSize(...)で State に保存
-
updateSize()をすぐ1回実行- 初回の描画直後にサイズを測る
useLayoutEffectなので、「画面に見える直前」にサイズが決まるイメージ
-
resizeイベントを登録- ウィンドウのサイズが変わると
updateSizeをもう一回実行 - つまり、ボックスのサイズも測り直す
- ウィンドウのサイズが変わると
-
クリーンアップでイベント解除
- コンポーネントが消えるときに
removeEventListener - これを書いておくことで、メモリリークを防げます 👍
- コンポーネントが消えるときに
4-4. なんで useEffect じゃなくて useLayoutEffect なの?🤔
useEffect でも、サイズを測ることはできます。
ただしタイミングが少し遅くて、
- 画面に一度「古いレイアウト」が出る
- そのあと
useEffectが動いて、サイズを測って、再描画
という流れになります。
レイアウトに関わる処理は、
「画面に見える瞬間より前に済ませておきたい」 ので、
useLayoutEffectのほうが向いています ✅
今回の例はシンプルなので、違いが分かりにくいかもですが、 「レイアウト計測・位置計算系は useLayoutEffect」 と覚えておくと良い感じです ✨
5️⃣ ちょっと遊んでみよう ✏️ ミニお題
ここまでできたら、少しアレンジしてみましょう 🧸
お題1:ボックスの高さを変えてみる
paddingをもっと増やしたり、- 文章を増やして、2〜3行にしてみたり
して、height の数字がどう変わるか試してみてください。
お題2:ボックスの幅を 80% にしてみる
style={{
width: "80%",
// ...
}}
に変えて、ウィンドウサイズを動かしたときの挙動を観察してみてください 👀
お題3:サイズに応じてメッセージを変える
たとえばこんな感じで、表示を変えてみるのも楽しいです 💬
let message = "ふつうサイズです ✨";
if (size !== null) {
if (size.width < 320) {
message = "ちょっと小さめサイズだよ 📱";
} else if (size.width > 600) {
message = "かなりワイド! 🖥️";
}
}
そして JSX で:
<p>{message}</p>
みたいにすると、 画面サイズに応じてテキストが変わる、ちょっとリッチな UI になります ✨
6️⃣ まとめ 🧵
この章でやったことを振り返ると…
-
refで「実際の DOM 要素」をつかまえた ✋ -
useLayoutEffectの中でgetBoundingClientRect()でサイズを取得Stateに保存して、画面に表示した
-
ウィンドウの
resizeイベントを使って、サイズを測り直すようにした -
レイアウト系の処理は
useLayoutEffectが向いている、という感覚をつかんだ
ここまでできれば、 「React + TypeScript で DOM をちゃんと意識したコードを書く」 っていう一歩をしっかり踏み出せています 👟✨
もし「ここもうちょっと詳しく聞きたい」「useEffect との比較をコードで見たい」などあれば、
続きも一緒に練習していきましょ〜 🥰💻