useRefを活用しよう
前回の記事では、useRef を使って DOM要素を直接参照する基本 と、そこから取得できる情報(サイズ・位置・クラス・属性など)を紹介しました。
しかし、実際の開発では「ただDOMを参照する」だけではなく、動的に変化する情報を監視したり操作したりするケースが多くあります。
例えば:
・コンテンツの高さが変わったら、自動的にレイアウトを調整したい
・ページをスクロールすると、現在どのセクションにいるかを検知してナビゲーションをハイライトしたい
・外部ライブラリ(チャートや地図など)の初期化にDOM要素を渡したい
このような場面では、useRef を組み合わせて「DOMの状態をリアルタイムで監視・活用」することが可能です。
今回の「活用編」では、boxサイズの動的監視を皮切りに、実務で役立つ useRef のテクニックを紹介していきます。
画像や動画のプレビューサイズをSVGに反映する
実務では、画像や動画のプレビュー領域のサイズを読み取り、それに応じて SVG にガイドラインや枠を描画するUIを実装することがあります。
例えば、画像編集画面で「トリミング枠」を表示したり、データ可視化で「オーバーレイのガイド線」を重ねたりするケースです。
実装イメージ
1. リストプレビュー領域(画像や動画を表示する box)のサイズをuseRefと ResizeObserver で監視する
2. 取得したサイズをSVGコンポーネントに渡して描画する
import { useRef, useEffect, useState } from "react";
function PreviewBox() {
const boxRef = useRef<HTMLDivElement | null>(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!boxRef.current) return;
// ResizeObserverでboxのサイズを監視
const resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
setSize({ width, height });
}
});
//boxRef.currentのサイズ変化を監視、
//変化があれば entries に結果が入ってコールバック実行
resizeObserver.observe(boxRef.current);
//クリーンアップ処理
return () => resizeObserver.disconnect();
}, []);
return (
<div
ref={boxRef}
style={{
width: "60%", // 親要素に依存してサイズが変わる
height: "300px",
border: "2px solid gray",
position: "relative",
}}
>
{/* 元のプレビュー画像 */}
<img
src="https://placehold.jp/400x300.png"
alt="preview"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
{/* サイズに応じて描画されるSVGオーバーレイ */}
<OverlaySVG width={size.width} height={size.height} />
</div>
);
}
type OverlayProps = {
width: number;
height: number;
};
const OverlaySVG = ({ width, height }: Props) => {
return (
<svg
width={width}
height={height}
style={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}}
>
{/* 外枠 */}
<rect
x="0"
y="0"
width={width}
height={height}
stroke="red"
strokeWidth="2"
fill="none"
/>
{/* 中央のガイドライン */}
<line
x1={width / 2}
y1={0}
x2={width / 2}
y2={height}
stroke="blue"
strokeDasharray="4"
/>
<line
x1={0}
y1={height / 2}
x2={width}
y2={height / 2}
stroke="blue"
strokeDasharray="4"
/>
</svg>
);
};
export default PreviewBox;
動きのポイント
・ResizeObserver を使っているため、ウィンドウサイズの変更や画像サイズの変動に即座に追随できます。
・OverlaySVG を別コンポーネントに切り出すことで、width と height を props 経由で渡し、描画ロジックを分離できます。
このパターンを使えば、「プレビュー領域にオーバーレイするガイド線・枠・選択範囲」 を自由に表現可能です。
リサイズ監視で重くなる場合の対策
ResizeObserver を使えばプレビュー領域のサイズ変化を自動で拾えますが、頻繁にリサイズが発生する要素を監視すると処理が重くなることがあります。
例えば、ウィンドウサイズをぐいぐい変えたり、動画のレスポンシブ表示で何度もリフローが発生する場合などです。
こうしたケースでは、以下の工夫を取り入れるとパフォーマンスを保ちやすくなります。
1. throttle / debounce を使って更新頻度を制御する
サイズが変わるたびに state 更新すると再レンダリングが多発します。
lodashのthrottleやdebounceを挟んで「200ms に1回だけ更新」などにすると、余計な再描画を防げます。
import { useRef, useEffect, useState } from "react";
import { throttle } from "lodash";
function useThrottledSize(ref: React.RefObject<HTMLElement>) {
const [size, setSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!ref.current) return;
const updateSize = throttle((entry: ResizeObserverEntry) => {
const { width, height } = entry.contentRect;
setSize({ width, height });
}, 200); // 200msごとに更新
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) updateSize(entry);
});
resizeObserver.observe(ref.current);
return () => {
observer.disconnect();
updateSize.cancel();
};
}, [ref]);
return size;
}
2. 必要なときだけ監視する
常に監視し続けるのではなく、プレビュー画面を表示しているときだけ
編集モードに入ったときだけなど 限定的に ResizeObserver を動かすと負荷を減らせます。
3. state ではなく ref に保持する
「描画に使わないけどサイズ情報は保持したい」という場合、state ではなく useRef に記録すれば再レンダリングを発生させずに済みます。
const sizeRef = useRef({ width: 0, height: 0 });
4. CSS で代替できる場合は CSS を優先
単に比率や見た目を合わせたいだけなら、aspect-ratio や object-fit、Flexbox などで済む場合もあります。
「監視が必要な場面」と「CSSで完結する場面」を見極めることが大切です。
まとめ
・ResizeObserver は強力だが多用すると重い
・更新頻度を間引いたり、監視タイミングを限定する工夫が有効
・必要な場面とそうでない場面を切り分けて使うのがベスト