O hirunewani blog

Ref callbackとcleanup functions

Created at

Ref callback、React v19で追加されたクリーンアップ関数、useEffectとの違いについて。また特にクリーンアップ関数を使ったRef callbackの実装例。

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>;