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?

【Qiita Advent Calender 2025企画】React Hooks を Hackしよう!【Part5: useRefをふかぼってみよう!】

0
Last updated at Posted at 2025-12-06

React を使っていると、「再レンダーを起こさずに値を保持したい」「DOM 要素に直接アクセスしたい」という場面に出くわすことがあります。そんなとき活躍するのが useRef フックです。本記事では、useRef の基礎から facebook/react リポジトリの実際のソースコードを参照した内部構造の解説、そして実践的なユースケースまでを徹底的に掘り下げます。

1. なぜ useRef が必要か

1.1 useState との違い

React では useState を使って状態を管理しますが、状態が変更されるたびにコンポーネントが再レンダーされます。
useState は表示に影響する値を管理するのに最適ですが、useRef は再レンダーが不要な値を保持するために使います。
useStateを使いすぎると...画面が再レンダーされすぎて画面がカクついたり、パフォーマンスが低下したりすることがあります。
しかし、次のような場合には再レンダーが不要です:

  • タイマーやインターバルの ID を保持したい
  • 前回のレンダー時の値を記憶したい
  • DOM 要素への参照を保持したい
// ❌ useState を使うと、値を更新するたびに再レンダーが発生
function Timer() {
  const [intervalId, setIntervalId] = useState(null);
  
  const start = () => {
    const id = setInterval(() => console.log('tick'), 1000);
    setIntervalId(id);  // 再レンダーが発生!(不要)
  };
  
  const stop = () => {
    clearInterval(intervalId);
  };
  // ...
}

// ✅ useRef なら再レンダーなしで値を保持
function Timer() {
  const intervalRef = useRef(null);
  
  const start = () => {
    intervalRef.current = setInterval(() => console.log('tick'), 1000);
    // 再レンダーは発生しない!
  };
  
  const stop = () => {
    clearInterval(intervalRef.current);
  };
  // ...
}

ただ...何でもかんでもuseRefに置き換えるのは良くありません。表示に影響する値は必ずuseStateで管理しましょう。

1.2 useRef の3つの特徴

useRef は次の3つの特徴を持っています:

特徴 説明
レンダー間で値を保持 通常の変数はレンダーごとにリセットされるが、ref は保持される
変更しても再レンダーしない ref.current を変更しても React は再レンダーをトリガーしない
ミュータブル state と違い、ref.current は直接書き換え可能
const ref = useRef(initialValue);
// ref = { current: initialValue }

1.3 useRef の2つの用途

useRef には主に2つの用途があります:

  1. 値の参照: 再レンダーに影響しない値を保持
  2. DOM の参照: DOM 要素に直接アクセス
// 用途1: 値の参照
const countRef = useRef(0);
countRef.current++;  // 再レンダーなしでカウントアップ

// 用途2: DOM の参照
const inputRef = useRef(null);
// ...
<input ref={inputRef} />
// ...
inputRef.current.focus();  // DOM メソッドを直接呼び出し

💡 ポイント:
useRef は「再レンダーをトリガーしない useState」のように考えることができます。ただし、表示に影響する値には使わないでください。

2. useRef の内部構造を徹底解剖

useRef を使うたびに、React の内部では複数のモジュールが連携して動いています。この章では、facebook/react リポジトリの実際のコードを追いながら、useRef がどのように動作するかを解説します。

2.0 全体像: useRef が動く仕組み

まず、useRef の処理フローを把握しましょう:

📦 useRef(ref オブジェクトの作成)
   ↓
🔗 Hook ノードに保存
   ↓
🎯 DOM の ref 属性(オプション)
   ↓
📍 commitAttachRef(DOM 要素の設定)

驚くほどシンプルな実装なので、すぐに理解できるはずです。

2.1 エントリポイント: packages/react/src/ReactHooks.js

まず、useRef を呼ぶとどこに飛ぶのか?

// packages/react/src/ReactHooks.js
export function useRef<T>(initialValue: T): {current: T} {
  const dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

何が起きているか?

  1. resolveDispatcher() で現在の Dispatcher を取得

    • Dispatcher は Hooks の実装を切り替える「交通整理役」
  2. Dispatcher の useRef を呼び出し

    • マウント時と更新時で異なる実装に委譲

2.2 Dispatcher の実装: マウント時と更新時

// packages/react-reconciler/src/ReactFiberHooks.js

const HooksDispatcherOnMount: Dispatcher = {
  // ...
  useRef: mountRef,  // ← マウント時
  // ...
};

const HooksDispatcherOnUpdate: Dispatcher = {
  // ...
  useRef: updateRef,  // ← 更新時
  // ...
};

useState と同様に、マウント時と更新時で異なる関数が呼ばれます。

2.3 コア実装: mountRefupdateRef

これが useRef の核心部分です。シンプル!

// packages/react-reconciler/src/ReactFiberHooks.js

function mountRef<T>(initialValue: T): {current: T} {
  // ステップ1: Hook ノードを作成
  const hook = mountWorkInProgressHook();
  
  // ステップ2: ref オブジェクトを作成
  const ref = {current: initialValue};
  
  // ステップ3: Hook に保存
  hook.memoizedState = ref;
  
  return ref;
}

function updateRef<T>(initialValue: T): {current: T} {
  // 既存の Hook を取得
  const hook = updateWorkInProgressHook();
  
  // 保存されている ref オブジェクトをそのまま返す
  return hook.memoizedState;
}

ポイント解説

mountRef(初回レンダー時):

  1. mountWorkInProgressHook() で新しい Hook ノードを作成
  2. {current: initialValue} という単純なオブジェクトを作成
  3. Hook の memoizedState に保存して返す

updateRef(2回目以降のレンダー):

  1. updateWorkInProgressHook() で既存の Hook を取得
  2. 保存されている ref オブジェクトをそのまま返す
  3. initialValue は完全に無視される!

💡ポイント
updateRefinitialValue を引数として受け取りますが、完全に無視しています。これが「初期値は初回のみ使用される」という動作の理由です。

Hook ノードの構造

Fiber (コンポーネント)
  └─ memoizedState (最初の Hook)
       ├─ memoizedState: { current: 0 }  ← useRef の値
       ├─ baseState: null
       ├─ queue: null                     ← useRef は queue を使わない
       └─ next → 次の Hook

useState と違い、useRefqueue を使いません。単純に memoizedState に ref オブジェクトを保存するだけです。

2.4 なぜ ref.current を変更しても再レンダーされないのか?

// packages/react-reconciler/src/ReactFiberHooks.js

function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};  // ← ただの JavaScript オブジェクト
  hook.memoizedState = ref;
  return ref;
}

答えは単純です:ref はただの JavaScript オブジェクトだからです。

  • useStatesetStatedispatchSetState を呼び、再レンダーをスケジュールする
  • useRef は単なるオブジェクトを返すだけ。変更を検知する仕組みがない
// これは React に何も伝えない
ref.current = newValue;  // ただのプロパティ代入

// これは React に再レンダーを依頼する
setState(newValue);  // dispatchSetState が呼ばれる

2.5 DOM ref の仕組み: commitAttachRef

<input ref={inputRef} /> と書いた時、何が起きるのか?

// packages/react-reconciler/src/ReactFiberCommitEffects.js

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    let instanceToUse;
    
    // ステップ1: DOM インスタンスを取得
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(finishedWork.stateNode);
        break;
      // ... 他のケース
      default:
        instanceToUse = finishedWork.stateNode;
    }
    
    // ステップ2: ref に DOM を設定
    if (typeof ref === 'function') {
      // コールバック ref の場合
      finishedWork.refCleanup = ref(instanceToUse);
    } else {
      // オブジェクト ref の場合
      ref.current = instanceToUse;
    }
  }
}

2つの ref パターン

// パターン1: オブジェクト ref(useRef が返すもの)
const inputRef = useRef(null);
<input ref={inputRef} />
// → ref.current = DOM要素

// パターン2: コールバック ref
<input ref={(node) => { console.log(node); }} />
// → ref(DOM要素) が呼ばれる

2.6 ref の更新タイミング: markRef

いつ ref が更新されるのか?

// packages/react-reconciler/src/ReactFiberBeginWork.js

function markRef(current: Fiber | null, workInProgress: Fiber) {
  const ref = workInProgress.ref;
  
  if (ref === null) {
    // ref が削除された場合
    if (current !== null && current.ref !== null) {
      workInProgress.flags |= Ref | RefStatic;  // Ref effect をスケジュール
    }
  } else {
    if (typeof ref !== 'function' && typeof ref !== 'object') {
      throw new Error(
        'Expected ref to be a function, an object returned by React.createRef(), or undefined/null.',
      );
    }
    // ref が追加または変更された場合
    if (current === null || current.ref !== ref) {
      workInProgress.flags |= Ref | RefStatic;  // Ref effect をスケジュール
    }
  }
}

Ref フラグの役割

// packages/react-reconciler/src/ReactFiberFlags.js
export const Ref = 0b0000000000000000000001000000000;  // 512
  • Ref フラグが立つと、コミットフェーズで commitAttachRef が呼ばれる
  • DOM がマウントされた後に ref が設定される

2.7 useImperativeHandle の仕組み

親コンポーネントに公開するメソッドをカスタマイズする

// packages/react-reconciler/src/ReactFiberHooks.js

function imperativeHandleEffect<T>(
  create: () => T,
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
) {
  if (typeof ref === 'function') {
    // コールバック ref の場合
    const refCallback = ref;
    const inst = create();  // ← カスタムハンドルを生成
    const refCleanup = refCallback(inst);
    return () => {
      if (typeof refCleanup === 'function') {
        refCleanup();
      } else {
        refCallback(null);
      }
    };
  } else if (ref !== null && ref !== undefined) {
    // オブジェクト ref の場合
    const refObject = ref;
    const inst = create();  // ← カスタムハンドルを生成
    refObject.current = inst;  // ← DOM の代わりにカスタムオブジェクトを設定
    return () => {
      refObject.current = null;
    };
  }
}

function mountImperativeHandle<T>(
  ref: {current: T | null} | ((inst: T | null) => mixed) | null | void,
  create: () => T,
  deps: Array<mixed> | void | null,
): void {
  const effectDeps =
    deps !== null && deps !== undefined ? deps.concat([ref]) : null;

  mountEffectImpl(
    UpdateEffect | LayoutStaticEffect,
    HookLayout,  // ← useLayoutEffect と同じタイミングで実行
    imperativeHandleEffect.bind(null, create, ref),
    effectDeps,
  );
}

💡ポイント
useImperativeHandle は内部的に useLayoutEffect と同様の仕組みを使っています。これにより、DOM がマウントされた直後(ブラウザのペイント前)にカスタムハンドルが設定されます。

2.8 全体の流れを図解

フロー1: useRef によるマウント

フロー2: DOM ref の設定

フロー3: ref.current の更新(再レンダーなし)

2.9 まとめ: useRef の内部構造

useRef が動く仕組みの3ステージ

  1. mountRef: {current: initialValue} を作成し Hook に保存
  2. updateRef: 保存された ref オブジェクトをそのまま返す
  3. commitAttachRef: DOM ref の場合、DOM 要素を ref.current に設定

useState との実装の違い

項目 useState useRef
queue の使用 ✅ UpdateQueue を使用 ❌ 使用しない
更新時の処理 複雑な reducer 処理 単に memoizedState を返すだけ
再レンダー setState で発火 発火しない
値の比較 Object.is で比較 比較なし

3. 代表的ユースケース

3.1 タイマー・インターバルの管理

function Stopwatch() {
  const [time, setTime] = useState(0);
  const intervalRef = useRef<number | null>(null);
  
  const start = () => {
    if (intervalRef.current !== null) return;
    intervalRef.current = setInterval(() => {
      setTime(t => t + 1);
    }, 1000);
  };
  
  const stop = () => {
    if (intervalRef.current === null) return;
    clearInterval(intervalRef.current);
    intervalRef.current = null;
  };
  
  useEffect(() => {
    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
    };
  }, []);
  
  return (
    <div>
      <p>{time}</p>
      <button onClick={start}>開始</button>
      <button onClick={stop}>停止</button>
    </div>
  );
}

3.2 前回の値を記憶

function usePrevious<T>(value: T): T | undefined {
  const ref = useRef<T>();
  
  useEffect(() => {
    ref.current = value;
  });
  
  return ref.current;  // 前回のレンダー時の値
}

function Counter() {
  const [count, setCount] = useState(0);
  const prevCount = usePrevious(count);
  
  return (
    <div>
      <p>現在: {count}, 前回: {prevCount}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

3.3 DOM 操作

function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);
  
  useEffect(() => {
    inputRef.current?.focus();
  }, []);
  
  return <input ref={inputRef} placeholder="自動フォーカス" />;
}

3.4 子コンポーネントのメソッドを公開

// 子コンポーネント
interface InputHandle {
  focus: () => void;
  clear: () => void;
}

function FancyInput({ ref }: { ref: React.Ref<InputHandle> }) {
  const inputRef = useRef<HTMLInputElement>(null);
  
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => {
      if (inputRef.current) inputRef.current.value = '';
    },
  }));
  
  return <input ref={inputRef} />;
}

// 親コンポーネント
function Parent() {
  const inputRef = useRef<InputHandle>(null);
  
  return (
    <>
      <FancyInput ref={inputRef} />
      <button onClick={() => inputRef.current?.focus()}>フォーカス</button>
      <button onClick={() => inputRef.current?.clear()}>クリア</button>
    </>
  );
}

4. パフォーマンスと注意点

4.1 レンダー中に ref を読み書きしない

// ❌ レンダー中に ref を読み書き
function BadComponent() {
  const ref = useRef(0);
  ref.current++;  // ❌ 副作用!
  return <div>{ref.current}</div>;  // ❌ 予測不能な動作
}

// ✅ イベントハンドラや Effect 内で読み書き
function GoodComponent() {
  const ref = useRef(0);
  
  useEffect(() => {
    ref.current++;  // ✅ Effect 内なら OK
  });
  
  const handleClick = () => {
    console.log(ref.current);  // ✅ イベントハンドラ内なら OK
  };
  
  return <button onClick={handleClick}>Click</button>;
}

4.2 高コストな初期化の回避

// ❌ 毎回 VideoPlayer が生成される(使われないのに)
function Video() {
  const playerRef = useRef(new VideoPlayer());  // 毎レンダーで new
  // ...
}

// ✅ 遅延初期化パターン
function Video() {
  const playerRef = useRef<VideoPlayer | null>(null);
  
  if (playerRef.current === null) {
    playerRef.current = new VideoPlayer();  // 初回のみ実行
  }
  // ...
}

4.3 表示に影響する値には useState を使う

// ❌ ref.current を表示しても更新されない
function BadCounter() {
  const countRef = useRef(0);
  return (
    <div>
      <p>{countRef.current}</p>  {/* 更新されない! */}
      <button onClick={() => countRef.current++}>+1</button>
    </div>
  );
}

// ✅ 表示に影響する値は state で管理
function GoodCounter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

5. トラブルシューティング

5.1 独自コンポーネントへの ref が取得できない

// ❌ 独自コンポーネントにそのまま ref を渡しても動かない
function Parent() {
  const ref = useRef(null);
  return <MyInput ref={ref} />;  // エラー or 警告
}

// ✅ props で ref を受け取り、内部の要素に転送
function MyInput({ ref }: { ref: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} />;
}

5.2 初期値が null の時の TypeScript 型

// 初期値が null で、後から設定する場合
const inputRef = useRef<HTMLInputElement | null>(null);

// 使用時に null チェックが必要
inputRef.current?.focus();

// または Non-null assertion(確実に設定されている場合のみ)
inputRef.current!.focus();

6. まとめ

この記事で解説した内容は、公式ドキュメントとfacebook/react リポジトリの以下のファイルに基づいています:

useRef のエントリポイント

  • packages/react/src/ReactHooks.js
    • useRef のエクスポート関数

コア実装

  • packages/react-reconciler/src/ReactFiberHooks.js
    • mountRef: 初回レンダー時の ref 作成
    • updateRef: 更新時の ref 取得
    • mountImperativeHandle / updateImperativeHandle: カスタムハンドル

DOM ref の処理

  • packages/react-reconciler/src/ReactFiberCommitEffects.js
    • commitAttachRef: DOM 要素を ref に設定
    • safelyAttachRef / safelyDetachRef: エラーハンドリング付き

ref フラグの管理

  • packages/react-reconciler/src/ReactFiberBeginWork.js
    • markRef: Ref effect のスケジューリング
  • packages/react-reconciler/src/ReactFiberFlags.js
    • Ref フラグの定義


  • useRef{current: value} という単純なオブジェクトを返す
  • 内部実装は驚くほどシンプル:Hook ノードに ref オブジェクトを保存するだけ
  • ref.current を変更しても再レンダーされない(React が検知しない)
  • DOM ref は commitAttachRef でコミットフェーズに設定される
  • 表示に影響する値には useState を使い、再レンダー不要な値には useRef を使う

使い分けの指針: 「この値が変わったら画面を更新したいか?」— Yes なら useState、No なら useRef

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?