940 文字
5 分

Ref callbackとcleanup functions

2025-01-19

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#

useEffectuseRefなどと組み合わせることでに似たようなことが出来る。 特に 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>;
Ref callbackとcleanup functions
https://blog.ohirunewani.com/posts/react-ref-callback-patterns/
作者
hrdtbs
公開日
2025-01-19
ライセンス
CC BY-NC-SA 4.0