ReactのuseRefでDOM操作を最小限に抑える方法と実例解説

React開発において、DOM操作はアプリケーションのパフォーマンスやコードの可読性に大きく影響を与えます。Reactは基本的に仮想DOMを活用してUIを効率的に更新しますが、それでも一部のケースではDOMへの直接的な操作が必要になることがあります。このとき、適切に操作しなければパフォーマンスが低下したり、意図しない動作が発生する可能性があります。

そこで登場するのが、ReactのuseRefフックです。useRefを活用することで、必要最低限のDOM操作を実現し、アプリケーションのパフォーマンスを維持することが可能です。本記事では、useRefの基本的な使い方から応用例までを網羅し、Reactでの開発効率を大幅に向上させる具体的な方法を解説します。

目次

useRefの基本と特徴


ReactのuseRefフックは、状態の変化による再レンダリングを伴わずに、値やDOM要素への参照を保持するためのツールです。以下にuseRefの基本的な特徴を説明します。

値の参照を保持する


useRefは、再レンダリング間でデータを維持することができます。例えば、関数呼び出し間で値を追跡するカウンタや、以前の値を保持するために使用されます。この場合、useRefに格納された値は状態管理のように変化しても再レンダリングを引き起こしません。

DOM要素の参照


useRefは主にDOM要素へのアクセスに利用されます。useRefを使うと、Reactコンポーネントの中から特定のDOM要素に直接アクセスし、フォーカスを設定したりスタイルを操作したりすることが可能です。

import React, { useRef } from 'react';

function Example() {
  const inputRef = useRef(null);

  const focusInput = () => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>フォーカスを設定</button>
    </div>
  );
}

useRefの返り値


useRefは、オブジェクトを返します。このオブジェクトにはcurrentプロパティが含まれており、これが参照先の値またはDOM要素を指します。

基本的な構文

const refContainer = useRef(initialValue);
  • initialValueには、参照の初期値を指定します(DOM操作の場合はnull)。
  • refContainer.currentで現在の値にアクセス可能です。

useRefはシンプルでありながら、適切に使うことでReactアプリケーションの効率を大幅に高めることができます。次節では、useRefuseStateの違いを見ていきます。

useRefとuseStateの違い

Reactでは、状態管理にuseStateがよく使用されますが、useRefとは目的や動作が異なります。ここでは、それぞれの特徴と使い分け方を解説します。

状態管理の仕組み


useStateは、コンポーネントの状態を管理し、状態が変化するたびにコンポーネントを再レンダリングします。一方で、useRefは状態の変化がUIの再レンダリングに影響を与えない場合に使用します。

import React, { useState, useRef } from 'react';

function CounterExample() {
  const [stateCount, setStateCount] = useState(0);
  const refCount = useRef(0);

  const incrementState = () => {
    setStateCount(stateCount + 1); // 再レンダリングが発生
  };

  const incrementRef = () => {
    refCount.current += 1; // 再レンダリングは発生しない
    console.log(refCount.current); // コンソールで確認
  };

  return (
    <div>
      <h3>useState Count: {stateCount}</h3>
      <button onClick={incrementState}>Stateでカウントアップ</button>

      <h3>useRef Count: {refCount.current}</h3>
      <button onClick={incrementRef}>Refでカウントアップ</button>
    </div>
  );
}

useStateとuseRefの適用例

useStateを使うべきケース

  • 状態変化がUIに影響を与える場合(例: カウントの更新を表示する)。
  • 再レンダリングが必要な場合(例: ボタンの押下回数に応じてスタイルを変更)。

useRefを使うべきケース

  • DOM要素への直接参照が必要な場合(例: 特定の入力フィールドにフォーカスを設定)。
  • 状態が変化しても再レンダリングを避けたい場合(例: 関数間での非表示カウンタの管理)。

パフォーマンスの観点からの使い分け

  • 再レンダリングが頻繁に発生する場面では、useRefを使用することでパフォーマンスを改善できます。
  • ただし、UIに反映される変更が必要な場合は、useStateを使用するのが適切です。

これらの違いを理解することで、状況に応じた適切なフックの選択が可能となり、効率的なReact開発が実現できます。次節では、DOM操作におけるuseRefの具体的な活用法を詳しく見ていきます。

DOM操作におけるuseRefの役割

Reactでは、仮想DOMを通じてUIを操作することが推奨されますが、特定の状況では直接DOM操作が必要になる場合があります。このような場合にuseRefは便利なツールとなります。以下では、DOM操作におけるuseRefの具体的な役割と活用法を解説します。

DOM要素への参照の設定


useRefを使うと、特定のDOM要素を参照できるようになります。これにより、以下のような操作が可能になります。

  • 要素にフォーカスを設定する。
  • 要素のスタイルやプロパティを直接変更する。
  • 要素の位置やサイズなどの情報を取得する。

基本的な使用例: フォーカスの設定


次のコード例は、ボタンをクリックしたときに入力フィールドへフォーカスを設定する方法を示しています。

import React, { useRef } from 'react';

function FocusExample() {
  const inputRef = useRef(null);

  const handleFocus = () => {
    if (inputRef.current) {
      inputRef.current.focus(); // 入力フィールドにフォーカスを設定
    }
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="ここにフォーカス" />
      <button onClick={handleFocus}>フォーカスを設定</button>
    </div>
  );
}

条件付きレンダリングでのuseRefの活用


動的にレンダリングされる要素にもuseRefを使用できます。この場合、DOM要素が確実に存在するタイミングで参照が利用できるようにすることが重要です。

例: モーダルウィンドウの初期フォーカス

import React, { useRef, useState } from 'react';

function ModalExample() {
  const [isOpen, setIsOpen] = useState(false);
  const modalRef = useRef(null);

  const openModal = () => {
    setIsOpen(true);
    setTimeout(() => {
      if (modalRef.current) {
        modalRef.current.focus();
      }
    }, 0); // DOMの確定を待つ
  };

  return (
    <div>
      <button onClick={openModal}>モーダルを開く</button>
      {isOpen && (
        <div
          ref={modalRef}
          tabIndex={-1} // フォーカス可能にするため
          style={{ background: 'rgba(0, 0, 0, 0.8)', color: '#fff', padding: '20px' }}
        >
          モーダルコンテンツ
        </div>
      )}
    </div>
  );
}

注意点: Reactの仮想DOMとの整合性


useRefを使った直接的なDOM操作は便利ですが、Reactの仮想DOMを経由しないため、次の点に注意が必要です。

  • DOMを直接操作することでReactの状態とUIの整合性が崩れる可能性があります。
  • 必要最小限の操作に留め、Reactが管理していない要素にのみ適用するのが理想です。

次節では、useRefを活用した具体的なフォーム操作の実例を紹介します。

useRefを活用したフォーム操作の実例

フォーム操作において、useRefを活用することで、再レンダリングを伴わない効率的な制御が可能になります。以下では、入力フィールドのフォーカス制御や、入力内容のクリアといった典型的なシナリオでのuseRefの利用例を紹介します。

フォーカス制御


フォーム入力時、特定のフィールドにフォーカスを自動的に設定するケースがあります。これをuseRefで実現すると、パフォーマンスに優れた方法で制御できます。

例: フォーカスの自動設定

import React, { useRef, useEffect } from 'react';

function AutofocusForm() {
  const nameInputRef = useRef(null);

  useEffect(() => {
    if (nameInputRef.current) {
      nameInputRef.current.focus(); // 初期表示時にフォーカスを設定
    }
  }, []);

  return (
    <form>
      <label>
        名前:
        <input ref={nameInputRef} type="text" placeholder="名前を入力してください" />
      </label>
      <button type="submit">送信</button>
    </form>
  );
}

入力内容のクリア


useRefを使うと、フォームの値をクリアする際に状態管理を介さずにDOM要素に直接アクセスできます。これにより、シンプルなコードで効率的な処理が可能です。

例: 入力値のリセット

import React, { useRef } from 'react';

function ClearForm() {
  const inputRef = useRef(null);

  const clearInput = () => {
    if (inputRef.current) {
      inputRef.current.value = ''; // 入力内容をクリア
    }
  };

  return (
    <form>
      <label>
        メールアドレス:
        <input ref={inputRef} type="email" placeholder="例: example@example.com" />
      </label>
      <button type="button" onClick={clearInput}>リセット</button>
    </form>
  );
}

複数フィールドの操作


複数の入力フィールドがあるフォームで、useRefを配列またはオブジェクトで管理することで、柔軟な操作が可能になります。

例: 動的なフォームフィールドの管理

import React, { useRef } from 'react';

function MultiFieldForm() {
  const refs = useRef({});

  const handleFocus = (field) => {
    if (refs.current[field]) {
      refs.current[field].focus(); // 指定されたフィールドにフォーカスを設定
    }
  };

  return (
    <form>
      <label>
        名前:
        <input ref={(el) => (refs.current.name = el)} type="text" />
      </label>
      <label>
        メール:
        <input ref={(el) => (refs.current.email = el)} type="email" />
      </label>
      <button type="button" onClick={() => handleFocus('name')}>名前フィールドにフォーカス</button>
      <button type="button" onClick={() => handleFocus('email')}>メールフィールドにフォーカス</button>
    </form>
  );
}

まとめ


フォーム操作におけるuseRefの活用は、再レンダリングを伴う状態管理を避けつつ、柔軟で効率的な制御を可能にします。次節では、useRefを使用したパフォーマンス最適化について詳しく解説します。

useRefを使ったパフォーマンス最適化

Reactアプリケーションでは、頻繁な再レンダリングがパフォーマンスに影響を与える場合があります。useRefは、このような場面で役立つツールであり、状態を保持しつつ不要な再レンダリングを防ぐことで、アプリケーションのパフォーマンスを向上させます。

再レンダリングを防ぐ仕組み


useRefは、再レンダリングをトリガーしない保持メカニズムを提供します。useStateと異なり、useRef内の値が更新されてもコンポーネントは再レンダリングされません。これにより、特定の操作でのみ必要なデータを効率的に管理できます。

例: 非同期カウンタ


以下のコードでは、クリックイベントの回数をuseRefで管理し、パフォーマンスに優れた設計を実現しています。

import React, { useRef } from 'react';

function Counter() {
  const countRef = useRef(0);

  const increment = () => {
    countRef.current += 1; // 値を更新
    console.log(`現在のカウント: ${countRef.current}`);
  };

  return (
    <div>
      <button onClick={increment}>カウントアップ</button>
      <p>カウントの値はコンソールを確認してください</p>
    </div>
  );
}

パフォーマンスが向上するケース

1. タイマーやイベントリスナーの管理


useRefは、非同期処理やイベントリスナーを管理する際に便利です。例えば、タイマーIDやイベントリスナーをuseRefで保持すると、再レンダリングが発生してもこれらの値を失うことがありません。

import React, { useRef, useEffect } from 'react';

function Timer() {
  const timerIdRef = useRef(null);

  useEffect(() => {
    timerIdRef.current = setInterval(() => {
      console.log('1秒ごとにログ');
    }, 1000);

    return () => clearInterval(timerIdRef.current); // クリーンアップ
  }, []);

  return <div>タイマーが作動中です。コンソールを確認してください。</div>;
}

2. 頻繁な値の更新


ユーザー操作によって頻繁に更新される値をuseRefで管理することで、パフォーマンスを維持しつつ状態を追跡できます。

3. 値の一時的な保持


あるイベント中にだけ必要な一時的なデータを保持し、再レンダリングの必要がない場合に適しています。

注意点

  • DOM操作以外への過剰な利用を避ける: 状態管理とUI更新を混同しないように、useRefの適切な用途を見極めることが重要です。
  • 同期性の確保: 非同期処理の結果が意図したタイミングで反映されるよう、設計を慎重に行う必要があります。

useRefによるパフォーマンス最適化のまとめ


useRefは、頻繁な状態更新や非同期操作が絡む場面で、不要な再レンダリングを防ぎ、Reactアプリケーションのパフォーマンスを向上させる強力なツールです。次節では、useRefとカスタムフックを組み合わせた高度な応用例を紹介します。

useRefとカスタムフックの組み合わせ

useRefをカスタムフックと組み合わせることで、コードの再利用性や保守性を向上させることができます。カスタムフックを利用すると、特定のロジックをコンポーネントから分離し、独立したモジュールとして管理することが可能です。以下に、useRefを活用したカスタムフックの実例を紹介します。

カスタムフックの基本構造


Reactのカスタムフックは、useで始まる名前の関数で、フックの機能をラップしたものです。useRefをカスタムフック内で利用することで、汎用的な機能を提供できます。

例: 自動フォーカス設定用カスタムフック


以下は、特定のDOM要素に自動的にフォーカスを設定するカスタムフックの例です。

import { useRef, useEffect } from 'react';

function useAutoFocus() {
  const ref = useRef(null);

  useEffect(() => {
    if (ref.current) {
      ref.current.focus();
    }
  }, []);

  return ref;
}

function AutoFocusInput() {
  const inputRef = useAutoFocus();

  return (
    <input ref={inputRef} type="text" placeholder="自動でフォーカスが設定されます" />
  );
}

このカスタムフックuseAutoFocusは、どのコンポーネントでも再利用可能で、コードの重複を減らします。

例: デバウンス入力用カスタムフック


ユーザーがフォームに入力するたびにデータが更新されると、アプリケーションのパフォーマンスが低下する場合があります。この問題を解決するために、useRefを利用してデバウンス(入力後一定時間経過してから処理を実行する)を実装します。

import { useRef, useEffect } from 'react';

function useDebounce(callback, delay) {
  const timeoutRef = useRef(null);

  const debounce = (...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    timeoutRef.current = setTimeout(() => {
      callback(...args);
    }, delay);
  };

  useEffect(() => {
    return () => clearTimeout(timeoutRef.current); // クリーンアップ
  }, []);

  return debounce;
}

function SearchBar() {
  const handleSearch = (query) => {
    console.log(`検索クエリ: ${query}`);
  };

  const debounceSearch = useDebounce(handleSearch, 500);

  return (
    <input
      type="text"
      placeholder="検索キーワードを入力"
      onChange={(e) => debounceSearch(e.target.value)}
    />
  );
}

このuseDebounceフックは、どのイベント処理にも適用可能で、ユーザーエクスペリエンスを向上させます。

注意点

  • カスタムフックの分離: カスタムフックを作成する際には、特定のコンポーネントに依存しない設計を心掛けましょう。
  • 過剰な抽象化を避ける: 汎用性を高めすぎると、フックが複雑になりすぎる可能性があります。シンプルで明確な目的を持たせることが重要です。

まとめ


useRefをカスタムフックと組み合わせることで、再利用可能なロジックを作成し、Reactアプリケーションの開発効率を大幅に向上させることができます。次節では、useRefを使用する際の一般的な注意点とベストプラクティスについて解説します。

useRefを使用する際の注意点

useRefは、React開発において非常に便利なツールですが、その特性ゆえに注意すべきポイントも多く存在します。ここでは、useRefを適切に使用するための注意点とベストプラクティスを解説します。

注意点

1. 状態管理の代替として使わない


useRefは状態管理のためのツールではありません。useRef内の値を直接変更してもコンポーネントは再レンダリングされません。そのため、UIに反映させる必要があるデータにはuseStateを使用すべきです。

import React, { useRef, useState } from 'react';

function IncorrectUsage() {
  const refCount = useRef(0);
  const [stateCount, setStateCount] = useState(0);

  const incrementRef = () => {
    refCount.current += 1;
    console.log(`Ref Count: ${refCount.current}`); // UIには表示されない
  };

  const incrementState = () => {
    setStateCount(stateCount + 1); // UIが更新される
  };

  return (
    <div>
      <button onClick={incrementRef}>Refでカウントアップ</button>
      <button onClick={incrementState}>Stateでカウントアップ</button>
      <p>State Count: {stateCount}</p>
    </div>
  );
}

2. 過剰なDOM操作の依存を避ける


Reactは仮想DOMを活用してUIを効率的に管理しますが、useRefによる直接的なDOM操作は、仮想DOMの設計思想と矛盾する場合があります。可能な限り、Reactの状態管理と仮想DOMによるUI更新を優先しましょう。

3. 初期値の設定


useRefの初期値は明示的に設定することが推奨されます。特にDOM操作で利用する場合、初期値をnullに設定しておくとエラーを防ぎやすくなります。

const ref = useRef(null); // 初期値を明示

4. useEffectとの連携を意識


useRefを使ってDOM要素にアクセスする場合、要素が確実にレンダリングされた後で操作を行う必要があります。このため、useEffectとの組み合わせが基本となります。

import React, { useRef, useEffect } from 'react';

function Example() {
  const inputRef = useRef(null);

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus(); // コンポーネントのマウント後にフォーカスを設定
    }
  }, []);

  return <input ref={inputRef} type="text" />;
}

ベストプラクティス

1. 必要最低限の範囲で使用


useRefは特定の目的にのみ使用し、一般的な状態管理やレンダリング管理には使用しないようにしましょう。

2. コンポーネント間での参照共有を避ける


複数のコンポーネント間で同じuseRefを共有する設計は、コードの複雑化やバグの原因になります。必要に応じてプロパティを渡すか、コンテキストを活用してください。

3. リファレンスのクリア


アンマウント時には、useRefを利用している場合もクリーンアップ処理を行いましょう。特にタイマーやイベントリスナーをuseRefで管理している場合に重要です。

import React, { useRef, useEffect } from 'react';

function TimerExample() {
  const timerRef = useRef(null);

  useEffect(() => {
    timerRef.current = setInterval(() => console.log('Timer running'), 1000);

    return () => clearInterval(timerRef.current); // クリーンアップ
  }, []);

  return <div>タイマー作動中</div>;
}

まとめ


useRefは、Reactアプリケーションで効率的なDOM操作や非同期処理を実現する重要なツールです。ただし、使いすぎるとReactの設計思想と矛盾し、コードのメンテナンス性が低下する可能性があります。これらの注意点を守りつつ、useRefを適切に活用しましょう。

次節では、useRefを用いた実用的な演習問題を紹介します。

演習問題: useRefでツールチップを実装

この演習では、useRefを活用して、ツールチップを表示するシンプルなコンポーネントを作成します。ツールチップは、ユーザーが特定の要素にマウスをホバーした際に追加情報を表示するために使われます。

要件

  • ボタンにマウスをホバーすると、ツールチップが表示される。
  • ツールチップはボタンの上に位置し、ホバーを外すと非表示になる。
  • ツールチップの位置はボタンのサイズと位置に基づいて動的に計算される。

コード例: ツールチップコンポーネント

以下のコードでは、useRefを利用してボタン要素の位置情報を取得し、ツールチップの位置を動的に調整しています。

import React, { useState, useRef } from 'react';

function Tooltip() {
  const [isTooltipVisible, setTooltipVisible] = useState(false);
  const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
  const buttonRef = useRef(null);

  const showTooltip = () => {
    if (buttonRef.current) {
      const rect = buttonRef.current.getBoundingClientRect();
      setTooltipPosition({
        top: rect.top - 30, // ボタンの上にツールチップを配置
        left: rect.left + rect.width / 2,
      });
      setTooltipVisible(true);
    }
  };

  const hideTooltip = () => {
    setTooltipVisible(false);
  };

  return (
    <div style={{ position: 'relative', padding: '100px' }}>
      <button
        ref={buttonRef}
        onMouseEnter={showTooltip}
        onMouseLeave={hideTooltip}
        style={{ padding: '10px 20px' }}
      >
        ホバーしてください
      </button>
      {isTooltipVisible && (
        <div
          style={{
            position: 'absolute',
            top: tooltipPosition.top,
            left: tooltipPosition.left,
            transform: 'translate(-50%, -100%)',
            padding: '5px 10px',
            backgroundColor: 'black',
            color: 'white',
            borderRadius: '4px',
            whiteSpace: 'nowrap',
          }}
        >
          これはツールチップです
        </div>
      )}
    </div>
  );
}

export default Tooltip;

コードのポイント

1. `useRef`で要素を参照


ボタン要素にrefを設定し、その位置情報を取得するためにgetBoundingClientRect()メソッドを使用しています。

2. マウスイベントで表示と非表示を制御

  • onMouseEnter: ツールチップを表示し、位置を動的に設定します。
  • onMouseLeave: ツールチップを非表示にします。

3. ツールチップの位置調整


ツールチップはボタンの中央上部に表示されるように位置を計算し、CSSのtransformプロパティを使用して微調整しています。

演習のポイント

  • 動的な位置計算: ボタンの位置が動的に変わる場合でもツールチップが正しい位置に表示されるようにする。
  • スタイルのカスタマイズ: ツールチップの見た目を改善するスタイルを試す。
  • アクセシビリティの向上: キーボード操作やスクリーンリーダーに対応させる。

発展課題

  • 複数のボタンにツールチップを適用できる汎用的なコンポーネントに改良する。
  • ツールチップの内容を動的に変更する機能を追加する。
  • ツールチップをアニメーションで表示・非表示にする。

この演習を通じて、useRefを活用したDOM要素の操作とReactでの動的なUI設計の理解を深めましょう。次節では、記事全体のまとめに移ります。

まとめ

本記事では、ReactのuseRefフックを活用してDOM操作を最小限に抑え、効率的にアプリケーションを構築する方法について解説しました。useRefの基本的な使い方から、フォーム操作やパフォーマンス最適化、カスタムフックとの組み合わせ、実用的なツールチップの実装例までを網羅しました。

useRefは、再レンダリングを伴わないデータの管理や、直接的なDOM操作を可能にする強力なツールです。ただし、Reactの仮想DOMの設計思想に沿って適切に使用することが重要です。

今回学んだ内容を実践することで、Reactアプリケーションの効率とパフォーマンスを向上させ、より保守性の高いコードを書くスキルを身につけられるでしょう。React開発をさらに深めるために、この記事で紹介した演習や発展課題にもぜひ挑戦してみてください。

コメント

コメントする

目次