【ITニュース解説】Coding Challenge Practice - Question 16
2025年09月26日に「Dev.to」が公開したITニュース「Coding Challenge Practice - Question 16」について初心者にもわかりやすく解説しています。
ITニュース概要
Reactで、要素にフォーカスが当たっているかを検知する`useFocus`フックの作り方を解説。このフックを使えば、親要素が子要素のフォーカス状態を把握し、その情報に基づいて表示内容を動的に変えられる。イベントリスナーでフォーカスin/outを監視する。
ITニュース解説
Webアプリケーションを開発していると、ユーザーが画面上のどの要素に注目しているか、つまり「フォーカス」が当たっているかを知りたい場面がよく出てくる。例えば、入力欄にカーソルが当たっている間だけ特別なメッセージを表示したり、複数の入力欄があるグループ全体にフォーカスがあるかを判定して、そのグループの状態に応じて見た目を変化させたりといったケースだ。今回解説する「useFocus」というカスタムフックは、まさにこのような状況で、ある要素、そしてその要素に含まれる子孫要素のいずれかにフォーカスが当たっているかどうかを簡単に検知できるようにするものだ。
このフックは、Reactのコンポーネントの中で再利用可能なロジックをまとめる「カスタムフック」として実装される。useFocusフックは、監視したいHTML要素への参照と、その要素にフォーカスが当たっているかどうかを示す真偽値の二つの情報を提供する。
まず、このフックの定義から見てみよう。
export function useFocus<T extends HTMLElement>(): [Ref<T>, boolean]
ここで、<T extends HTMLElement>は、このフックが任意のHTML要素で使えるようにするための型引数だ。Ref<T>は、T型のHTML要素への参照を意味し、booleanはフォーカスが当たっているかどうかを示す真偽値だ。つまり、このフックを使うと、監視対象の要素への参照と、フォーカス状態という二つの値が得られる。
次に、フックの内部実装を見ていこう。このフックは、ReactのuseRef、useState、useEffectという三つのフックを組み合わせて作られる。
-
useRefで要素への参照を作成:const ref = useRef<T>(null);useRefは、画面上の特定のHTML要素を直接操作したり参照したりするために使う。ここでは、refという名前の参照オブジェクトを作成し、初期値としてnullを設定している。このrefを後で監視したいHTML要素に設定することで、その要素をプログラムから参照できるようになる。 -
useStateでフォーカス状態を管理:const [isFocused, setIsFocused] = useState(false);useStateは、コンポーネントの「状態」を管理するためのフックだ。ここでは、isFocusedという名前の変数にフォーカス状態(真偽値)を保持し、setIsFocusedという関数を使ってその状態を更新する。初期値はfalse、つまり最初はフォーカスが当たっていない状態とする。 -
useEffectでイベントリスナーを設定・解除:useEffectは、コンポーネントが画面に表示された後に行うべき処理(データの取得やイベントリスナーの設定など)や、画面から消える前の片付け(イベントリスナーの解除など)を記述するためのフックだ。useFocusの主要なロジックはすべてこのuseEffectの中に書かれる。-
DOMノードの取得:
const node = ref.current;if (!node) return;useEffectの中で、まずref.currentを使って、useRefで作成した参照が指す実際のHTML要素(DOMノード)を取得する。もし何らかの理由で要素が見つからなければ、それ以上の処理は行わない。 -
フォーカスイベントを処理する関数: フォーカスが入ったときと外に出たときに実行する二つの関数を定義する。
const handleFocusIn = () => setIsFocused(true);このhandleFocusIn関数は、フォーカスが監視対象の要素(またはその子孫要素)に入ったときに呼び出される。実行されると、setIsFocused(true)によってisFocusedの状態がtrueに更新され、フォーカスが当たっていることが示される。const handleFocusOut = (e: FocusEvent) => { if (node && !node.contains(e.relatedTarget as Node)) { setIsFocused(false) } };このhandleFocusOut関数は、フォーカスが監視対象の要素(またはその子孫要素)から外れたときに呼び出される。ここで重要なのは、フォーカスが「完全に」外に出たかどうかを正確に判断するロジックだ。FocusEventオブジェクトにはe.relatedTargetというプロパティが含まれており、これはフォーカスがどこへ移動したかを示す要素だ。例えば、複数の入力欄があるdiv要素を監視しているとして、最初の入力欄から二つ目の入力欄へフォーカスが移動した場合、e.relatedTargetは二つ目の入力欄を指す。node.contains(e.relatedTarget as Node)は、監視対象のnode(例えばdiv要素)が、フォーカスが移動した先の要素(e.relatedTarget)を子孫として含んでいるかどうかを判定する。 もしdiv要素内のinput要素間でフォーカスが移動した場合、node.contains(e.relatedTarget)はtrueを返す。この時はまだdiv要素の内部にフォーカスがある状態なので、isFocusedはtrueのままにしておく必要がある。 しかし、もしinput要素からdiv要素の外にある全く別の要素へフォーカスが移動した場合、node.contains(e.relatedTarget)はfalseを返す。この時、!node.contains(e.relatedTarget as Node)という条件式がtrueとなり、「div要素と子孫要素からフォーカスが完全に外れた」と判断し、setIsFocused(false)によってisFocusedの状態をfalseに更新する。この仕組みにより、親要素全体へのフォーカス状態を正確に追跡できる。 -
イベントリスナーの登録:
node.addEventListener("focusin", handleFocusIn);node.addEventListener("focusout", handleFocusOut);focusinイベントは、要素自身やその子孫要素にフォーカスが入ったときに発生する。focusoutイベントは、要素自身やその子孫要素からフォーカスが外れたときに発生する。これらのイベントリスナーを監視対象のnodeに追加することで、上述の関数が適切なタイミングで呼び出されるようにする。 -
クリーンアップ関数:
return () => { node.removeEventListener("focusin", handleFocusIn); node.removeEventListener("focusout", handleFocusOut); };useEffectは、戻り値として関数を返すことができる。この関数は「クリーンアップ関数」と呼ばれ、コンポーネントが画面から消えるときや、useEffectが再度実行される前に呼び出される。ここでイベントリスナーを解除することは非常に重要だ。イベントリスナーを解除せずに放置すると、コンポーネントが画面から消えた後もイベントが残り続け、メモリリークや予期せぬ動作の原因となる可能性があるため、必ず片付けを行う必要がある。
-
最後に、このuseFocusフックをコンポーネントで利用する例を見てみよう。
export function App() { const [ref, isFocused] = useFocus<HTMLDivElement>() return <div ref={ref}> <input placeholder="First Input"/> <input placeholder="Second Input" /> {isFocused && <p>focused</p>} </div> }
Appコンポーネントでは、まずuseFocus<HTMLDivElement>()を呼び出し、div要素への参照(ref)とフォーカス状態(isFocused)を取得している。そして、<div ref={ref}>のように、取得したrefを監視したいdiv要素に設定している。このdiv要素の中には二つのinput要素が含まれている。
{isFocused && <p>focused</p>}という部分は、isFocusedがtrueの場合にのみ、「focused」というテキストを含む<p>要素が画面に表示されるようにする「条件付きレンダリング」の例だ。
これにより、ユーザーが「First Input」または「Second Input」のどちらかにフォーカスすると、div要素全体がフォーカスされていると判断され、「focused」のテキストが表示される。そして、ユーザーがこれらの入力欄から完全にフォーカスを外すと、「focused」のテキストは非表示になる。
このように、useFocusフックを使うことで、特定の要素とその子孫要素全体へのフォーカス状態を簡単に監視し、それに基づいてUIを動的に変化させることが可能となる。これは、ユーザーインタフェースの応答性を高め、より使いやすいアプリケーションを構築する上で非常に役立つ機能だ。