Ref callbackとcleanup functions
Ref callback、React v19で追加されたクリーンアップ関数、useEffectとの違いについて。また特にクリーンアップ関数を使ったRef callbackの実装例。
Table of Contents
Ref callback
Reactでは、ref
属性に関数を渡すことが出来る。
<div
ref={node => {
console.log(node);
}}
/>
これを利用することで、DOM要素がマウントされたときに何かしらの処理を行ったり、DOM要素を参照することが出来る。
https://ja.react.dev/reference/react-dom/components/common#ref-callback
Ref callbackのcleanup関数
React v19で、ref
コールバック関数が返す関数をクリーンアップ関数として扱うようになった。
const ref = (node: HTMLDivElement | null) => {
if (node) {
console.log("Mounted");
return () => {
console.log("Unmounted");
};
}
};
クリーンアップ関数は、対象のDOMノードがアンマウントされたときに呼び出される。
Ref callbackとuseEffect
useEffect
はuseRef
などと組み合わせることでに似たようなことが出来る。
特にReact 19以降では、クリーンアップ関数の追加により、ほぼ完全に同等のことが出来るようになった。
ただし、useEffect
がReactライフサイクルに依存しているのに対して、ref
コールバックはDOMノードのライフサイクルに依存しており、完全に同じになることはない。
Reactのライフサイクルに沿ってDOMノードを処理したい場合、例えば複数のDOMノードを組み合わせたい場合などは、useEffect
が適している可能性がある。一方で、多くのケースではDOMノードとReactのライフサイクルの関心を分離できるref
コールバックの方が、例えばイベントの登録や解除の漏れが発生しにくく、適しているように思う。
Ref callbackの例
Ref callbackを利用した例、特にクリーンアップ関数を利用したものを意図的に多くの載せている。
要素がマウントされたらフォーカスさせる
<input
ref={node => {
node?.focus();
}}
/>
要素のサイズを取得する
const [width, setWidth] = useState(0);
const masureRef = (node) => {
consrt observer = new ResizeObserver((entries) => {
setWidth(entries[0].contentRect.width);
});
observer.observe(node);
return () => {
observer.disconnect();
};
}
return <div ref={masureRef}>Width: {width}</div>;
要素がビューポート内に入ったか監視する
const [isVisible, setIsVisible] = useState(false);
const ref = (node: HTMLDivElement | null) => {
if (node) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
}
});
observer.observe(node);
return () => observer.disconnect();
}
};
return <div ref={ref}>{isVisible ? "Visible" : "Not visible"}</div>;
要素の変更を監視する
const [text, setText] = useState("Initial Text");
const ref = (node: HTMLDivElement | null) => {
if (node) {
const observer = new MutationObserver(() => {
console.log("DOM changed:", node.textContent);
});
observer.observe(node, { childList: true, subtree: true });
return () => observer.disconnect();
}
};
return (
<div ref={ref} contentEditable>
{text}
</div>
);
要素を全画面表示する
const ref = (node: HTMLDivElement | null) => {
const controller = new AbortController();
if (node) {
const enterFullscreen = () => {
node.requestFullscreen?.();
};
node.addEventListener("click", enterFullscreen, {
signal: controller.signal,
});
return () => controller.abort();
}
};
return (
<div
ref={ref}
style={{
width: "300px",
height: "200px",
cursor: "pointer",
}}
>
Click to go fullscreen!
</div>
);
クリップボードにコピーする
const ref = (node: HTMLButtonElement | null) => {
if (node) {
const controller = new AbortController();
const handleCopy = () => {
navigator.clipboard.writeText(node.textContent || "");
};
node.addEventListener("click", handleCopy, {
signal: controller.signal,
});
return () => controller.abort();
}
};
return <button ref={ref}>Copy to Clipboard</button>;
ドラッグ&ドロップを監視する
const ref = (node: HTMLDivElement | null) => {
const controller = new AbortController();
if (node) {
const handleDragStart = (event: DragEvent) => {
event.dataTransfer?.setData("text/plain", "Dragged Content");
};
node.draggable = true;
node.addEventListener("dragstart", handleDragStart, {
signal: controller.signal,
});
return () => controller.abort();
}
};
return (
<div
ref={ref}
style={{
cursor: "move",
}}
>
Drag Me
</div>
);
カメラの映像を表示する
const ref = (node: HTMLVideoElement | null) => {
if (node) {
navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
node.srcObject = stream;
});
return () => {
if (node.srcObject instanceof MediaStream) {
node.srcObject.getTracks().forEach(track => track.stop());
}
};
}
};
return <video ref={ref} autoPlay />;
要素のリサイズを監視する
const [size, setSize] = useState({ width: 0, height: 0 });
const ref = (node: HTMLDivElement | null) => {
if (node) {
const observer = new ResizeObserver(([entry]) => {
const { width, height } = entry.contentRect;
setSize({ width, height });
});
observer.observe(node);
return () => observer.disconnect();
}
};
return (
<div>
<div
ref={ref}
style={{
width: "200px",
height: "200px",
resize: "both",
overflow: "auto",
}}
/>
<p>
Width: {size.width}px, Height: {size.height}px
</p>
</div>
);
要素をアニメーションさせる
const ref = (node: HTMLDivElement | null) => {
if (node) {
const animation = node.animate(
[{ transform: "translateX(0)" }, { transform: "translateX(100px)" }],
{ duration: 1000, iterations: Infinity }
);
return () => animation.cancel();
}
};
return <div ref={ref}>Auto moving</div>;