【実務・中級編】 forwardRefを用いたPropsとRefの受け渡し – React実践ガイド

どうも、君のチームのチーフアーキテクトだ。
今日は、Reactの中級者が必ず一度はハマり、そして「なるほど、こう書くのか!」と腑に落ちるテーマについて話そう。そう、`forwardRef`を用いたPropsとRefの型定義、そしてその裏側の挙動についてだ。

実務でコードを書いていると、「親コンポーネントから、子コンポーネントの内部にある特定のDOM(例えば、``やモーダルのルート要素)に直接フォーカスを当てたい、あるいはスクロール位置を制御したい」という要件に必ずぶち当たる。

ここで適当に書くと、TypeScriptの残酷な赤波線エラーに阻まれるか、あるいは「動くには動くが、モジュールのカプセル化を完全に破壊したスパゲッティコード」が完成することになる。

今日の記事では、Reactの内部でブラウザがどう動いているのかというローレベルな仕組みから、実務でそのままコピペして使える堅牢な型定義のパターンまで、余すところなく伝授しよう。コーヒー片手に、じっくり読んでくれ。

—

なぜ `forwardRef` が必要なのか? — データの単方向バインディングという「鉄の掟」

Reactの基本思想はシンプルだ。「データは上から下へ(Props)、イベントは下から上へ(コールバック)」流れる。DOMの参照(Ref)も基本的にはこのルールに従い、同じコンポーネントツリーの中で完結させるべきものだ。

しかし、現実は非情だ。
例えば、自作のカスタム入力コンポーネント(``)を作ったとする。親コンポーネント側で「画面表示時にこのカスタム入力に自動でフォーカス(`inputRef.current.focus()`)を当てたい」と思ったとき、通常のPropsとして渡すことはできない。なぜなら、`ref`は通常のProps(`prop.children`など)とは別枠の、Reactの内部予約プロパティとして扱われているからだ。

ここで登場するのが `forwardRef` である。
「親から渡されたRefを、自分のコンポーネントをスルーして、内部のネイティブDOM(``など)にそのまま中継(Forward)する」ための特別なラッパー関数、それが `forwardRef` なのだ。

ブラウザの裏側の話:Refの本質とDOMノード

ブラウザのレンダリングエンジン(WebKitやBlinkなど)の視点に立ってみよう。
ブラウザはHTMLをパースしてDOMツリーを作り、CSSOMと結合してレイアウトを計算する。Reactの仮想DOM(Virtual DOM)は、この実DOMを効率的に操作するための抽象化レイヤーに過ぎない。

`useRef` や `createRef` が保持している `.current` というプロパティの正体は、実際のブラウザ上のメモリ上に存在するDOMノードへの直接の参照(ポインタ)である。
親から子へRefを渡すということは、Reactの仮想DOMの境界線を越えて、親が子(の内部のDOM)のメモリ上の実体に直接触る権利を委譲することを意味する。だからこそ、Reactは「おいおい、本当に中身を覗き見してもいいんだな?」という安全確認のために、明示的な `forwardRef` という宣言を求めているわけだ。

—

実践!TypeScript環境における `forwardRef` の型定義パターン

さて、ここからが本題だ。TypeScriptで `forwardRef` を使おうとすると、多くのエンジニアがその独特なジェネリックの構文に頭を抱える。「どっちがDOMの型で、どっちがPropsの型なんだっけ?」と。

結論から言おう。`forwardRef` の型定義のシグネチャは、以下の順番を体に叩き込めば怖くない。

forwardRef((props, ref) => { … })

百聞は一見にしかず。実務でそのまま使える、極めて堅牢な「カスタムテキスト入力コンポーネント」のコードを見てほしい。

コピペして使える!実践的サンプルコード

import React, { forwardRef, useId } from ‘q’; // ※qはreactのタイポじゃないが、手癖に気をつけろ。正しくは ‘react’ だ。

// 1. コンポーネントが受け取る独自のPropsを定義する
export interface CustomInputProps {
label: string;
error?: string;
// ここに onClick や onChange などのネイティブな型を含めても良い
placeholder?: string;
}

/

  • 2. forwardRef を用いたコンポーネントの定義
  • ジェネリックの第一引数 : 親に公開するRefのターゲット(DOMの型)
  • ジェネリックの第二引数 CustomInputProps: このコンポーネントが受け取る独自のProps

/
export const CustomInput = forwardRef(
({ label, error, placeholder }, ref) => {
// アクセシビリティ(a11y)を考慮して一意のIDを生成するシニアの気配り
const id = useId();

return (

{/
受け取った ref を、内部のネイティブ 要素にそのまま結合する。
これで親コンポーネントからのリモートコントロールが可能になる。
/}

{error && (

{error}

)}

);
}
);

// デバッグやReact DevToolsでの表示名(DisplayName)の設定を忘れないのがプロの仕事
CustomInput.displayName = ‘CustomInput’;

親側(使用する側)のコード

親側でこのコンポーネントを使うときは、以下のように `useRef` に適切なDOM型を指定して呼び出す。

import React, { useRef, useEffect } from ‘react’;
import { CustomInput } from ‘./CustomInput’;

export const ParentComponent: React.FC = () => {
// 子コンポーネントの内部にある の型を指定する
const inputRef = useRef(null);

const handleFocusClick = () => {
// .current が存在することを確認し(オプショナルチェイニング)、フォーカスを当てる
inputRef.current?.focus();
};

useEffect(() => {
// マウント時に自動フォーカスする要件があるとしよう
inputRef.current?.focus();
}, []);

return (

非);
};

—

チーフアーキテクトからの実践的なアドバイス(アンチパターンと回避策)

この `forwardRef`、非常に強力な反面、使い方を誤るとコンポーネントの「カプセル化」という美しいアーキテクチャを完全に破壊する。現場でよく見る「やってはいけないアンチパターン」を共有しておこう。

1. DOMの内部構造を親に完全に露出させない

  • 今回の例では `` 自体にrefを渡したが、場合によっては「内部の特定のdivやボタンのref」を渡したくなることがある。しかし、それをやりすぎると、子が内部構造を変更した瞬間に親のコードがぶっ壊れる(密結合の誕生)。Refで渡すのは、あくまで「そのコンポーネントが代表する単一のインタラクション要素(inputやbuttonなど)」に留めるべきだ。

2. `displayName` をサボらない

  • TypeScriptで `forwardRef` を使ってアロー関数で書くと、React DevTools上でコンポーネント名が `ForwardRef` としか表示されなくなり、デバッグ地獄に陥る。上記のサンプルコードの通り、`CustomInput.displayName = ‘CustomInput’;` の1行は必ず書くこと。チームメンバーが君に感謝するはずだ。

—

まとめ

`forwardRef` は、Reactの「データの単方向性」という原則に対する、いわば「美しき例外措置」だ。
ブラウザのメモリ上にあるDOMを直接触るという強力な権限を持つからこそ、型安全性を担保し、適切なカプセル化の境界線を引く必要がある。

今回紹介した型定義のイディオム(``)と、`displayName` の設定は、明日からの君の実務でそのまま武器になるはずだ。

フロントエンドのアーキテクチャに「なんとなく動く」の妥協はいらない。裏側の仕組みまで理解した上で、美しく堅牢なコードを書き上げていこう。それじゃあ、次のコードレビューで会おう。

コメント

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