【テクニカル・上級編】 useRefによるDOMアクセスと値の保持 – React実践ガイド

`useRef`という名の「禁じ手」:Reactの再レンダリングを制御し、メモリを制するアーキテクチャ論

Reactを触り始めて数年、「`useState`で事足りる」というフェーズを過ぎた君たちが、次に直面するのは「リアクティブな状態管理の限界」だ。

「この値、画面の描画には影響させたくないが、後続の処理で参照したい」
「DOMの座標が欲しいが、そのたびにコンポーネントを再レンダリングさせたくない」

そんな時、我々アーキテクトが手を伸ばすのが`useRef`だ。しかし、このフックを「単なるDOMへのアクセス口」と捉えているなら、それはあまりにも勿体ない。今回は、`useRef`の深淵に触れ、Reactのレンダリングエンジンとメモリ効率を最適化する実践的なアプローチを共有する。

—

1. 仮想DOMの裏側で:`useRef`の本質

`useRef`は、平たく言えば「コンポーネントの生存期間中、一貫してメモリ上の同じアドレスを指し続けるミュータブルなコンテナ」だ。

重要なのは、`useRef`が保持する値の書き換え(`ref.current = …`)が、Reactのレンダリングプロセスを一切トリガーしないという点だ。`useState`は「値が変わったら画面を更新せよ」という宣言だが、`useRef`は「画面には関与しないが、ここにデータを置いておく」という、言わばフレームワークの外側に逃がした「裏口」である。

なぜこれが「禁じ手」なのか

Reactの哲学は「UIはStateの関数である(UI = f(state))」だ。`useRef`を乱用してStateの代わりに値を保持し始めると、UIとデータが乖離し、デバッグ不可能な「謎の挙動」を量産することになる。`useRef`はあくまで、レンダリングサイクルから切り離すべき「メタデータ」や「外部ライブラリのインスタンス」に限定すべきだ。

—

2. パフォーマンス最適化の極致:レンダリング負荷の削減

例えば、高速でスクロールするリストの現在位置を監視したいとする。`useState`で座標を管理すれば、スクロールのたびにコンポーネント全体が再レンダリングされ、ブラウザのメインスレッドを無駄に占有する。

以下は、`useRef`を用いてレンダリングを介さずに座標を追跡する設計だ。

import { useRef, useEffect } from ‘react’;

const ScrollTracker = () => {
// レンダリングに影響を与えない座標保持用コンテナ
const scrollPosition = useRef(0);

useEffect(() => {
const handleScroll = () => {
// 再レンダリングを発生させずに値を更新
scrollPosition.current = window.scrollY;

// 必要に応じて微細な最適化:
// 特定の閾値を超えた時だけStateを更新してUIを変える等の制御が可能
if (scrollPosition.current > 1000) {
console.log(“長距離スクロールを検知”);
}
};

window.addEventListener(‘scroll’, handleScroll);
return () => window.removeEventListener(‘scroll’, handleScroll);
}, []);

return

スクロールしてコンソールを確認せよ

;
};

—

3. 非同期処理における「最新値」の罠

Reactのフックにおける典型的なバグの一つが、`useEffect`のクロージャ内で古いStateを参照してしまう問題だ。依存配列を適切に設定しても、タイミングによっては競合が発生する。

そんな時、`useRef`を「最新値のバッファ」として利用するパターンは、現場で非常に重宝する。

const useLatest = (value) => {
const ref = useRef(value);

// レンダリングのたびに最新値をrefに同期する
// これにより、非同期コールバック内から常に「現時点」の値にアクセスできる
useEffect(() => {
ref.current = value;
}, [value]);

return ref;
};

// 使用例:非同期処理の完了時に、その時点の最新のStateを確実に取り出す
const MyComponent = ({ data }) => {
const latestData = useLatest(data);

const handleClick = () => {
setTimeout(() => {
// 3秒前の値ではなく、”今”のdataを参照できる
console.log(“現在のデータ:”, latestData.current);
}, 3000);
};

return ;
};

—

4. アーキテクトの戒め:注意すべきアンチパターン

最後に、現場で見てきた「悲劇」を避けるための戒めを伝えておく。

1. レンダリング中に`ref.current`を読み書きしてはいけない:
Reactのコンポーネント関数は純粋であるべきだ。レンダリング中に`ref.current`を書き換えると、Reactの並行レンダリング(Concurrent Mode)と衝突し、予測不能なバグを誘発する。必ず`useEffect`かイベントハンドラの中で行うこと。
2. DOMアクセスを「フック」の中に隠蔽しすぎない:
`useRef`でDOMに触れるのは、Canvas操作や複雑なアニメーション、サードパーティのDOM操作ライブラリをラップする場合のみに留めろ。宣言的なUI構築こそがReactの魂であり、`ref`による命令的なDOM操作はその対極にある。

—

結びに代えて

`useRef`は、Reactという抽象化された世界における「生メモリ」へのパスポートだ。
この強力なツールを、単なる「DOMへのポインタ」として使い潰すか、レンダリングサイクルを制御し、アプリケーションのパフォーマンスを限界まで引き出すための「設計の要」とするか。それは君たちの技術的感性にかかっている。

堅牢なアプリケーションは、こうした「Reactの作法」と「ブラウザの限界」を正しく理解したエンジニアの細やかな配慮から生まれるのだ。次は、この`useRef`を`useImperativeHandle`と組み合わせて、疎結合なコンポーネント設計を実践してみるといい。世界が変わるはずだ。

コメント

タイトルとURLをコピーしました