0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Reactのcallback refが返すcleanupを、同じDOMの再描画と型検査で確かめる

0
Last updated at Posted at 2026-11-03

Reactのcallback refは、DOM nodeを受け取るだけでなく、解除時に呼ぶcleanup関数を返せます。React 19で追加されたこの仕組みを、ResizeObserverの登録と解除で確認しました。

見たいのはunmountだけではありません。同じDOM nodeが残っていても、渡すcallbackの参照が変わると、前のcleanupと次の登録が動きました。

observerを作ったcallbackから解除関数を返す

次の関数は、callback refと観測用ログを作ります。legacy: falseならcleanupを返し、trueならnullを受け取ったときに解除します。

function makeTrackedRef(legacy) {
  let observer;
  function disconnect() {
    observer.disconnect();
    trace.active.delete(observer);
    trace.events.push('cleanup');
  }
  return node => {
    if (node === null) {
      trace.events.push('null');
      if (observer) disconnect();
      return;
    }
    observer = new ResizeObserver(() => {});
    observer.observe(node);
    trace.active.add(observer);
    trace.events.push('attach');
    if (!legacy) return disconnect;
  };
}

実際にブラウザのResizeObserverを作り、対象nodeをobserveしています。trace.activeには未解除として記録しているobserverを入れます。サイズ変更通知の回数は今回の比較対象にしていません。

同じcallbackを保つ版と、描画のたびに作り直す版を比べました。

export function RefExperiment({ stable, legacy }) {
  const [tick, setTick] = useState(0);
  const stableRef = useMemo(() => makeTrackedRef(legacy), [legacy]);
  const ref = stable ? stableRef : makeTrackedRef(legacy);
  return <section>
    <button id="rerender" onClick={() => setTick(previous => previous + 1)}>Render {tick}</button>
    <div id="observed" ref={ref}>Observed node</div>
  </section>;
}

ボタンを押すと親が再描画されますが、対象のdivは同じnodeのままです。最後にrootをunmountし、解除まで確認しました。

同じnodeでもcallbackを変えると解除される

build・callbackの作り方 初回ログ 再描画で追加されたログ unmountで追加されたログ
production、同じcallback、cleanupを返す attach なし cleanup
production、新しいcallback、cleanupを返す attach cleanup → attach cleanup
production、同じcallback、nullで解除 attach なし null → cleanup
開発root Strict Mode、同じcallback、cleanupを返す attach → cleanup → attach なし cleanup
開発root Strict Mode、新しいcallback、cleanupを返す attach → cleanup → attach cleanup → attach cleanup
開発root Strict Mode、同じcallback、nullで解除 attach → null → cleanup → attach なし null → cleanup

6条件とも、初回処理後と再描画後の未解除observer数は1、unmount後は0でした。callbackを作り直す版でもcleanupが対になっているので、今回の実装ではobserverが増え続けませんでした。

callback refの公式説明も、返したcleanup関数を解除時に呼ぶこと、callbackが変わる場合の解除・再登録、開発時の追加サイクルを区別しています。cleanupを返した版では、今回のログにnull呼び出しはありません。null側でも同じ解除が重ねて来ると期待しないようにします。

再描画前後でnode参照を直接比較し、同じDOMを維持したことを確かめました。callbackの実行回数と、DOMが作り直されたかどうかは分けて調べます。

値を暗黙に返すと型エラーになる場合がある

型でも違いを確認しました。TypeScript 7.0.2、@types/react 19.3.0、strictとreact-jsxを使っています。

import React from 'react';
const nodes = new Map<string, HTMLDivElement | null>();
export const view = <div ref={node => nodes.set('preview', node)} />;

Map.setはMapを返すため、このcallbackはvoidやcleanup関数を返す型へ適合せず、TS2322になりました。また、cleanupを() => nodes.delete('preview')と書いた版も、booleanを返すためTS2322です。

次のようにブロックを使い、cleanupから値を返さない版は通りました。

import React from 'react';
const nodes = new Map<string, HTMLDivElement>();
export const view = <div ref={node => {
  if (!node) return;
  nodes.set('preview', node);
  return () => { nodes.delete('preview'); };
}} />;

この型の比較はReact 19の移行ガイドでも説明されている変更に対応します。Mapへの格納が目的なら、その戻り値をref callbackの戻り値へ流さない書き方にします。

無限ループの対策とcleanupの確認は分ける

既存の記事では、画像プレビューのref callbackからstateを更新し続ける循環を扱いました。今回はstate更新をobserver callbackへ入れず、callbackの同一性と解除タイミングを比較しています。cleanupを返せば、任意のstate更新ループも解消するという検証ではありません。

検証日は2026年9月11日、React / React DOM 19.3.0、Node.js 24.15.0、esbuild 0.28.2、macOS arm64、Headless Chrome 152です。React Compilerは使っていません。production側にもStrict Modeのラッパーを付けて比較しましたが、開発時の追加サイクルは起きませんでした。observerの数は実験側の登録・解除記録で、ブラウザ内部のリソース使用量を測った値ではありません。

実験コード、6条件のログ、型検査3条件を残しました。refの動きを調べるときは、nodeが変わったかと、callbackの参照が変わったかを分けて確認できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?