useRefでReactのDOMを直接操作!コンポーネントの状態管理にも活用

useRefでReactのDOMを直接操作!コンポーネントの状態管理にも活用

ReactにおけるuseRefフックは、DOM要素への直接アクセス、コンポーネントの状態管理、そしてタイマーやアニメーション制御など、多岐にわたる用途を持つ強力なツールです。本記事では、useRefの基本的な使い方から応用的なテクニックまでを網羅的に解説し、React開発におけるuseRefの可能性を最大限に引き出す方法を提案します。

1. useRefとは何か?Reactにおける役割

useRefはReactのフックの一つで、コンポーネントのライフサイクル全体を通して値を保持できる「箱」のような役割を果たします。この「箱」に格納された値は、コンポーネントの再レンダリングによって初期化されることはありません。useStateフックがコンポーネントの状態を管理し、変更時に再レンダリングをトリガーするのに対し、useRefは値を保持するだけで再レンダリングを引き起こさない点が大きな違いです。

useRefの主な特徴:

  • ミュータブルな値の保持: useRefで作成されたオブジェクトの.currentプロパティは、JavaScriptのオブジェクトであるため、自由に値を変更できます。
  • 再レンダリングの抑制: .currentプロパティの値を変更しても、コンポーネントは再レンダリングされません。
  • コンポーネントのライフサイクル全体で値を保持: コンポーネントのマウントからアンマウントまで、useRefで保持された値は維持されます。

useRefの典型的な用途:

  • DOM要素への直接アクセス: 入力フィールドやボタンなどのDOM要素への参照を保持し、フォーカス操作や値の取得などに利用します。
  • コンポーネントの状態管理: 再レンダリングを引き起こしたくない値を保持し、例えばタイマーIDや前のpropsの値などを保持します。
  • 命令的なAPIとの統合: Canvas APIやWebGLなど、命令的なコードを実行する必要がある場合に、要素への参照を保持し、直接操作を行います。

2. useRefの基本的な使い方:DOM要素へのアクセス

useRefの最も一般的な使い方は、DOM要素へのアクセスです。以下の例は、入力フィールドにフォーカスを当てる方法を示しています。

“`jsx
import React, { useRef, useEffect } from ‘react’;

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

useEffect(() => {
// コンポーネントのマウント時にinputRefにフォーカスを当てる
inputRef.current.focus();
}, []); // 空の依存配列で、初回レンダリング時のみ実行

return (

);
}

export default FocusInput;
“`

コードの解説:

  1. useRef(null): useRefフックを呼び出し、初期値をnullに設定します。inputRefは、.currentプロパティを持つオブジェクトを返します。
  2. ref={inputRef}: 入力フィールドのref属性にinputRefを渡します。これにより、Reactは入力フィールドのDOM要素への参照をinputRef.currentに格納します。
  3. useEffect(() => { ... }, []): useEffectフックを使用し、コンポーネントのマウント時にのみフォーカスを当てる処理を実行します。空の依存配列[]を渡すことで、useEffectは初回レンダリング時のみ実行されます。
  4. inputRef.current.focus(): inputRef.currentを通じてDOM要素にアクセスし、.focus()メソッドを呼び出して入力フィールドにフォーカスを当てます。

ポイント:

  • ref属性は、DOM要素がマウントされた後にのみinputRef.currentに値を設定します。そのため、DOM要素へのアクセスはuseEffectなどのライフサイクルメソッド内で行う必要があります。
  • inputRef.currentの値は、DOM要素への直接の参照であるため、JavaScriptのDOM APIを自由に利用できます。

3. useRefを使ったコンポーネントの状態管理:再レンダリングを伴わない値の保持

useRefは、再レンダリングを伴わずに値を保持する必要がある場合に非常に役立ちます。例えば、カウンターの値を保持したり、前のpropsの値を保持したりする際に利用できます。

例1: カウンターの値を保持する

“`jsx
import React, { useRef, useState, useEffect } from ‘react’;

function Counter() {
const countRef = useRef(0);
const [renderCount, setRenderCount] = useState(0);

useEffect(() => {
countRef.current = countRef.current + 1;
console.log(“countRef.current:”, countRef.current); // 常にカウントアップ
}, [renderCount]); // renderCountが変更されるたびにuseEffectが実行

const incrementRenderCount = () => {
setRenderCount(renderCount + 1); // stateを更新して再レンダリング
}

return (

Render Count: {renderCount}

countRef.current: {countRef.current}

);
}

export default Counter;
“`

コードの解説:

  1. countRef = useRef(0): countRefを初期値0で初期化します。
  2. useEffect(() => { ... }, [renderCount]): renderCount stateが更新されるたびに、useEffect内のコードが実行されます。
  3. countRef.current = countRef.current + 1;: countRef.currentの値をインクリメントします。これは再レンダリングを引き起こしません。
  4. incrementRenderCount = () => { ... }: renderCount stateを更新する関数です。stateの更新は再レンダリングを引き起こします。

この例では、ボタンをクリックしてrenderCountを更新すると、コンポーネントが再レンダリングされ、countRef.currentの値も更新されます。しかし、countRef.currentの更新は再レンダリングを引き起こさないため、パフォーマンス上のオーバーヘッドを避けることができます。

例2: 前のPropsの値を保持する

“`jsx
import React, { useRef, useEffect } from ‘react’;

function PreviousValue(props) {
const prevPropsRef = useRef();

useEffect(() => {
prevPropsRef.current = props;
});

return (

Current Prop: {props.value}

Previous Prop: {prevPropsRef.current && prevPropsRef.current.value}

);
}

export default PreviousValue;
“`

コードの解説:

  1. prevPropsRef = useRef();: prevPropsRefを初期化します。初期値はundefinedになります。
  2. useEffect(() => { ... }): useEffectは、コンポーネントが再レンダリングされるたびに実行されます。
  3. prevPropsRef.current = props;: 現在のpropsの値をprevPropsRef.currentに保存します。これにより、次のレンダリング時に、前のpropsの値にアクセスできます。

この例では、PreviousValueコンポーネントに渡されるprops.valueの値が変更されるたびに、useEffectによってprevPropsRef.currentが更新されます。これにより、現在のpropsの値と前のpropsの値を比較したり、前のpropsの値に基づいて何らかの処理を実行したりすることができます。

4. useRefの応用:タイマーとアニメーションの制御

useRefは、タイマーやアニメーションのIDを保持するのに非常に役立ちます。これにより、コンポーネントがアンマウントされた際にタイマーやアニメーションを停止し、メモリリークを防ぐことができます。

例1: タイマーの制御

“`jsx
import React, { useState, useEffect, useRef } from ‘react’;

function Timer() {
const [time, setTime] = useState(0);
const timerIdRef = useRef(null);

useEffect(() => {
timerIdRef.current = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);

return () => {
  clearInterval(timerIdRef.current); // コンポーネントがアンマウントされる際にタイマーを停止
};

}, []);

return (

Time: {time}

);
}

export default Timer;
“`

コードの解説:

  1. timerIdRef = useRef(null): タイマーのIDを保持するためのtimerIdRefを初期化します。
  2. useEffect(() => { ... }, []): useEffectは、コンポーネントがマウントされた際にのみ実行されます。
  3. timerIdRef.current = setInterval(() => { ... }, 1000): setIntervalを使用して、1秒ごとにsetTime関数を呼び出し、time stateを更新します。タイマーのIDをtimerIdRef.currentに保存します。
  4. return () => { clearInterval(timerIdRef.current); };: useEffectのクリーンアップ関数として、コンポーネントがアンマウントされる際にclearIntervalを呼び出し、タイマーを停止します。これにより、メモリリークを防ぎます。

例2: アニメーションの制御

“`jsx
import React, { useRef, useEffect } from ‘react’;

function Animation() {
const animationFrameIdRef = useRef(null);
const elementRef = useRef(null);

useEffect(() => {
const element = elementRef.current;

const animate = () => {
  // アニメーションのロジックを実装
  element.style.transform = `translateX(${Math.sin(Date.now() / 1000) * 100}px)`;
  animationFrameIdRef.current = requestAnimationFrame(animate);
};

animationFrameIdRef.current = requestAnimationFrame(animate);

return () => {
  cancelAnimationFrame(animationFrameIdRef.current); // コンポーネントがアンマウントされる際にアニメーションを停止
};

}, []);

return (

);
}

export default Animation;
“`

コードの解説:

  1. animationFrameIdRef = useRef(null): requestAnimationFrameによって返されるIDを保持するためのanimationFrameIdRefを初期化します。
  2. elementRef = useRef(null): アニメーションを適用するDOM要素への参照を保持するためのelementRefを初期化します。
  3. useEffect(() => { ... }, []): useEffectは、コンポーネントがマウントされた際にのみ実行されます。
  4. const animate = () => { ... }: アニメーションのロジックを定義する関数です。この例では、要素を水平方向に振動させるアニメーションを実装しています。
  5. animationFrameIdRef.current = requestAnimationFrame(animate): requestAnimationFrameを使用して、ブラウザのレンダリングサイクルに合わせてアニメーションを実行します。requestAnimationFrameは、アニメーションフレームのIDを返します。
  6. return () => { cancelAnimationFrame(animationFrameIdRef.current); };: useEffectのクリーンアップ関数として、コンポーネントがアンマウントされる際にcancelAnimationFrameを呼び出し、アニメーションを停止します。

5. useRefを使用する際の注意点

useRefは非常に便利なフックですが、使用する際にはいくつかの注意点があります。

  • .currentの直接的な変更: useRefで作成されたオブジェクトの.currentプロパティはミュータブルであるため、直接値を変更できます。しかし、.currentの値を変更してもコンポーネントは再レンダリングされないため、変更がUIに反映されるためには、別途useStateフックなどで状態を管理する必要があります。
  • 副作用: useRefの値を変更すること自体は副作用ではありませんが、その値を使用してDOMを直接操作したり、タイマーを起動したりする場合は副作用となります。これらの副作用は、useEffectフック内で実行する必要があります。
  • 初期値の重要性: useRefを初期化する際には、適切な初期値を設定することが重要です。例えば、DOM要素への参照を保持する場合は、初期値をnullに設定しておくと、コンポーネントのマウント前にuseRef.currentにアクセスする際にエラーが発生するのを防ぐことができます。
  • パフォーマンス: useRefは、再レンダリングを引き起こさないため、パフォーマンス上のメリットがあります。しかし、過剰にuseRefを使用すると、コードの可読性が低下したり、状態管理が複雑になったりする可能性があります。useRefを使用する前に、本当に再レンダリングを抑制する必要があるのかを検討することが重要です。
  • 参照の維持: コンポーネントがアンマウントされた後もuseRefで保持された値は維持されます。これは、メモリリークの原因となる可能性があるため、コンポーネントがアンマウントされる際に、useRefで保持された参照を適切にクリアすることが重要です。例えば、タイマーIDを保持している場合は、useEffectのクリーンアップ関数でclearIntervalを呼び出してタイマーを停止する必要があります。

6. useRefと他のフックとの比較:useState, useMemo, useCallback

useRefは、Reactの他のフックと組み合わせて使用することで、より強力な機能を実装できます。ここでは、useState, useMemo, useCallbackとの比較を通して、useRefの役割をより深く理解しましょう。

  • useRef vs useState:

    • useState: コンポーネントの状態を管理し、状態が変更されると再レンダリングをトリガーします。UIの更新が必要な場合に適しています。
    • useRef: 値を保持するだけで、再レンダリングを引き起こしません。DOM要素へのアクセスや、再レンダリングを伴わない状態管理に適しています。

    使い分け: UIの更新が必要な場合はuseState、それ以外の場合はuseRefを検討します。例えば、入力フィールドの値はuseStateで管理し、入力フィールドへの参照はuseRefで管理する、という使い分けが一般的です。
    * useRef vs useMemo:

    • useMemo: 計算コストの高い処理の結果をメモ化し、依存配列の値が変更された場合にのみ再計算します。パフォーマンスの最適化に役立ちます。
    • useRef: 値を保持するだけで、計算結果をメモ化する機能はありません。

    使い分け: 計算結果を再利用したい場合はuseMemo、値を保持したい場合はuseRefを検討します。useRefは、useMemoで計算された値を保持するためにも利用できます。
    * useRef vs useCallback:

    • useCallback: 関数をメモ化し、依存配列の値が変更された場合にのみ新しい関数を作成します。子コンポーネントへの不要な再レンダリングを防ぐために役立ちます。
    • useRef: 関数をメモ化する機能はありません。

    使い分け: 関数を再利用したい場合はuseCallback、値を保持したい場合はuseRefを検討します。useRefは、useCallbackで作成された関数を保持するためにも利用できます。

7. useRefを使った高度なテクニック

useRefは、単にDOM要素へのアクセスや状態管理を行うだけでなく、より高度なテクニックにも応用できます。

  • 前の値を保持するカスタムフック:

    “`jsx
    import { useRef, useEffect } from ‘react’;

    function usePrevious(value) {
    const ref = useRef();
    useEffect(() => {
    ref.current = value;
    });
    return ref.current;
    }

    export default usePrevious;
    “`

    このカスタムフックを使用すると、コンポーネントのpropsやstateの前の値を簡単に取得できます。

    “`jsx
    import React, { useState } from ‘react’;
    import usePrevious from ‘./usePrevious’;

    function MyComponent() {
    const [count, setCount] = useState(0);
    const previousCount = usePrevious(count);

    return (

    Current Count: {count}

    Previous Count: {previousCount}

    );
    }

    export default MyComponent;
    “`
    * Canvas APIとの統合:

    “`jsx
    import React, { useRef, useEffect } from ‘react’;

    function CanvasComponent() {
    const canvasRef = useRef(null);

    useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext(‘2d’);

    // Canvas APIを使って描画処理を行う
    ctx.fillStyle = 'red';
    ctx.fillRect(10, 10, 50, 50);
    

    }, []);

    return (

    );
    }

    export default CanvasComponent;
    “`

    useRefを使用することで、Canvas要素への参照を保持し、Canvas APIを使って自由に描画処理を行うことができます。

  • Three.jsとの統合:

    useRefを使用することで、Three.jsのシーンやカメラ、レンダラーへの参照を保持し、Reactコンポーネント内でThree.jsの3Dグラフィックスを操作することができます。

これらの例は、useRefの可能性の一部に過ぎません。useRefを使いこなすことで、React開発における表現の幅を大きく広げることができます。

8. まとめ:useRefをマスターしてReact開発をレベルアップ

本記事では、useRefフックの基本的な使い方から応用的なテクニックまでを網羅的に解説しました。useRefは、DOM要素への直接アクセス、コンポーネントの状態管理、タイマーやアニメーションの制御など、多岐にわたる用途を持つ強力なツールです。

useRefをマスターすることで、以下のメリットが得られます。

  • パフォーマンスの向上: 不要な再レンダリングを抑制し、アプリケーションのパフォーマンスを向上させることができます。
  • 柔軟な状態管理: 再レンダリングを伴わない状態を管理し、より複雑なロジックを実装することができます。
  • 命令的なAPIとの統合: Canvas APIやWebGLなど、命令的なAPIをReactコンポーネント内でシームレスに利用することができます。
  • コードの可読性向上: useRefを適切に使用することで、コードの可読性を向上させることができます。

useRefは、React開発において非常に重要なフックの一つです。本記事で紹介した知識とテクニックを参考に、useRefを使いこなして、より効率的で洗練されたReactアプリケーションを開発してください。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール