【ITニュース解説】React Ref Problem: Ref Pointing to Multiple DOM Elements with CSS Media Query Hiding
2025年09月09日に「Dev.to」が公開したITニュース「React Ref Problem: Ref Pointing to Multiple DOM Elements with CSS Media Query Hiding」について初心者にもわかりやすいように丁寧に解説しています。
ITニュース概要
Reactで同じrefを複数のDOM要素に割り当てると、最後に描画された要素のみが参照される。CSSで要素を非表示にしてもDOMには残るため、意図せず非表示要素を操作してバグが起きる。要素ごとにrefを分けるか、条件付きで描画すれば回避可能。
ITニュース解説
Webアプリケーション開発で広く利用されているJavaScriptライブラリ「React」において、特定のDOM要素を直接操作するために使われる「Ref」という機能があります。このRefの仕様と、CSSによるレスポンシブデザインの実装が組み合わさった際に発生しうる、一見すると不可解なバグとその解決策について解説します。システムエンジニアを目指す上で、Reactのレンダリングの仕組みとDOM操作の基本を理解することは非常に重要です。
Reactでは通常、コンポーネントの状態(state)を通じてUIを宣言的に構築しますが、特定のDOM要素、つまりWebページを構成するHTMLタグに直接アクセスしたい場面も存在します。例えば、入力フォームに自動でフォーカスを当てたり、アニメーションを制御したり、要素の寸法を取得したりする場合です。このような目的で使われるのが「Ref」であり、コンポーネントが描画したDOM要素への参照を保持するための仕組みです。しかし、このRefには重要な制約があります。それは、一つのRefオブジェクトがそのcurrentプロパティで保持できるDOM要素の参照は、常に一つだけであるという点です。もし複数のDOM要素に同じRefを割り当てた場合、Reactは最後にレンダリングされた要素の参照をそのRefに設定します。
今回取り上げる問題は、このRefの性質が原因で発生したバグです。具体的な状況として、PCなどのデスクトップ画面用のUIと、スマートフォンなどのモバイル画面用のUIを別々のコンポーネントとして実装したケースを考えます。そして、画面の横幅に応じて、CSSのメディアクエリを使い、片方を表示し、もう片方をdisplay: none;で非表示にするという、一般的なレスポンシブ対応が行われていました。問題は、これらデスクトップ用とモバイル用の両方のコンポーネントに、同一のRef(例えばlocationRef)が割り当てられていたことです。
Reactがコンポーネントをレンダリングする際、JSXコードに記述された順に処理が進みます。仮にデスクトップ用コンポーネントの次にモバイル用コンポーネントが記述されていた場合、Reactは両方のコンポーネントをDOMツリー上に構築します。その結果、locationRef.currentが指し示すのは、最後にレンダリングされたモバイル用コンポーネントのDOM要素になります。ここで重要なのは、デスクトップ画面で閲覧している時、モバイル用コンポーネントはCSSによってdisplay: none;と指定され画面上は見えなくなっていますが、DOMツリーからは削除されておらず、存在し続けているという事実です。
この状態で、メニューの外側をクリックしたらメニューを閉じる、という機能を実装したとします。この機能は、クリックされた要素が、Refが指すメニュー要素の内部に含まれているかどうかを判定することで実現されます。しかし、デスクトップ画面ではlocationRefが画面に表示されていないモバイル用コンポーネントを指しているため、奇妙な動作を引き起こします。ユーザーが表示されているデスクトップ用のメニュー内をクリックしても、プログラムは「クリックされた場所は、locationRefが指す要素(非表示のモバイル用メニュー)の外側である」と判断してしまいます。結果として、メニュー内をクリックしたにもかかわらず、メニューが閉じてしまうという意図しない挙動が発生するのです。一方で、モバイル画面で表示している際は、locationRefは正しく表示されているモバイル用コンポーネントを指しているため、この問題は発生せず、バグの発見が遅れる原因にもなります。
この問題を解決するには、主に二つのアプローチが考えられます。一つ目は、デスクトップ用とモバイル用にそれぞれ別のRefを用意する方法です。例えばdesktopLocationRefとmobileLocationRefのように二つのRefを作成し、各コンポーネントに割り当てます。そして、クリックイベントを処理する関数内で、現在の画面幅をwindow.innerWidthなどで判定し、表示されているのがどちらのコンポーネントかを判断した上で、対応する正しいRefを使って内外判定を行います。これにより、常に表示されている要素を基準にロジックが実行されるようになります。
二つ目は、より根本的な解決策です。CSSで表示・非表示を切り替えるのではなく、Reactの機能である「条件付きレンダリング」を用いる方法です。これは、画面サイズなどの条件に応じて、デスクトップ用かモバイル用か、どちらか一方のコンポーネントだけをレンダリング(DOMツリーに構築)するという手法です。この方法を採用すれば、DOM上には常に一つのコンポーネントしか存在しないため、そもそも一つのRefが複数の要素に割り当てられるという状況自体を回避できます。不要なDOM要素を生成しないため、パフォーマンスの観点からも望ましい解決策と言えるでしょう。この事例は、ReactのRefの基本的な仕組みを正確に理解し、CSSによるスタイリングとReactのレンダリングライフサイクルがどのように連携するかを意識することの重要性を示しています。