【実務・中級編】 forwardRefとPropsの組み合わせ – React実践ガイド

やあ。今日もリポジトリのコードレビューで目がシバシバする頃か?
Reactを触り始めてから数年、一通りのコンポーネント分割や状態管理はマスターし、「俺はもう中級だ」と胸を張っている時期に、大体みんなこの壁にぶぶつかる。

――そう、「親から子コンポーネントの内部にあるDOM要素を直接いじりたい」という、実務では避けて通れない泥臭い要件だ。

例えば、カスタム入力フォームを作ったとき、エラーが起きたらそこにフォーカスを当てたい、あるいはアニメーションのために要素の高さを正確に測りたい。そんなとき、素直なPropsの渡し方だけでは、Reactの鉄の掟(単方向データフロー)に阻まれて手詰まりになる。

そこで登場するのが `forwardRef` だ。今日は、この `forwardRef` と TypeScript の型付けについて、実務の現場で明日からドヤ顔で使えるベストプラクティスを授けよう。

—

なぜ普通のPropsではDOM参照を渡せないのか?

まず大前提を整理しておこう。Reactにおいて、`props` というのは親から子へ「上から下へ」流れる水のようなものだ。

通常、親が持つ `useRef()` の実体(`ref.current`)を、子コンポーネントに通常のカスタムprop(例えば `` のように)として渡そうとしても、Reactはこれを許さない。正確には、Function Component自体にはDOM要素のインスタンスが存在しないため、`ref` という予約された特別なprop名以外では、うまくDOMノードへの参照をスルーさせることができないのだ。

そこで、Reactは `forwardRef` というAPIを用意した。
これを使うと、「親から渡された `ref` を、自分の内包する特定のDOM要素(あるいは別のコンポーネント)に『転送(forward)』する」ことができるようになる。

—

現場で即戦力になる実装パターン:カスタムInputコンポーネント

百聞は一見にしかずだ。実務でよくある「フォーカス制御機能付きのカスタムインプット」を例に、綺麗な型定義と実装を見てみよう。

以下のコードをそのままエディタに貼り付ければ、TypeScriptの恩恵を受けつつ完璧に動作する。

import React, { forwardRef, useRef, useImperativeHandle } from ‘react’;

/

  • 1. 子コンポーネントが受け取る独自のProps型を定義する
  • (ref自体の型はこの中に含めなくてよい)

/
interface CustomInputProps {
label: string;
placeholder?: string;
}

/

  • 2. forwardRefを使うときの型定義の極意
  • 第1引数に「DOM要素(または子コンポーネント)の型」、第2引数に「独自のProps型」を指定する。
  • ここをミスるとTypeScriptのコンパイラが泣くことになるので注意。

/
const CustomInput = forwardRef(
({ label, placeholder }, ref) => {
return (


{/
親から受け取った ref を、そのままネイティブの input 要素にぶっ刺す。
これで親からこの input の実体を直接操作できるようになる。
/}

);
}
);

// React DevToolsでコンポーネントツリーが見やすくなるおまじない(実務では必須!)
CustomInput.displayName = ‘CustomInput’;

// ==========================================
// 親コンポーネントでの使用例
// ==========================================
export const ParentComponent: React.FC = () => {
// HTMLInputElementを指すrefを作成
const inputRef = useRef(null);

const handleFocusClick = () => {
// ref.current が存在すれば、ネイティブのfocus()メソッドを叩く
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.style.border = ‘2px solid #3b82f6’; // ついでにスタイルも直接いじる例
}
};

return (

);
};

—

型定義(Generics)でハマりがちな罠

先ほどのコードで、一番重要なのはここだ。

forwardRef(…)

TypeScript初心者や、普段あまり型を意識しないエンジニアがやりがちなのが、`forwardRef` と型の順番を逆にしてしまうミスだ。

  • 第1ジェネリック型: 「何を参照(Ref)したいか」のDOM型(例: `HTMLInputElement`, `HTMLDivElement` など)
  • 第2ジェネリック型: 「通常のProps」の型(例: `CustomInputProps`)

この順番を間違えると、親側で `useRef(null)` と定義しているにもかかわらず、子側の `ref.current` の型が `unknown` になったり、全く関係ない型推論エラーが出て夜な夜な悶絶することになる。必ず「Refが先、Propsが後」と覚えておいてほしい。

—

もう一歩踏み込む:DOMを直接露出させたくない時の `useImperativeHandle`

さて、ここからがシニアとしてのワンポイントアドバイスだ。

上記のコードでは、子コンポーネントの `` という生(生々しい)DOM要素をそのまま親に露出させている。しかし、アーキテクチャの観点から言うと、「子コンポーネントの内部構造(HTMLのタグが何であるか)を親に知られたくない」「外部に公開する操作(API)を制限したい」というケースも多い。

そんなときは、`useImperativeHandle` を組み合わせて使え。

import React, { forwardRef, useRef, useImperativeHandle } from ‘react’;

// 親に公開したいメソッドのインターフェース
export interface CustomInputHandle {
focus: () => void;
clear: () => void;
}

interface AdvancedInputProps {
label: string;
}

const AdvancedInput = forwardRef(
({ label }, ref) => {
const internalInputRef = useRef(null);

// 親の ref からアクセスできるオブジェクトを独自に定義・制限する
useImperativeHandle(ref, () => ({
focus: () => {
internalInputRef.current?.focus();
},
clear: () => {
if (internalInputRef.current) {
internalInputRef.current.value = ”;
}
},
}));

return (


);
}
);

これによって、親コンポーネントは「DOMがどうなっているか」を完全に隠蔽されたまま、子側が許可した `focus()` や `clear()` という安全なメソッドだけを叩けるようになる。カプセル化の観点からも、大規模アプリケーションでは非常に美しい設計だ。

—

まとめ:forwardRefを使うべきか、使わざるべきか

最後に、実務で設計を行う上での心構えを伝えておく。

`forwardRef` は強力なカードだが、「Reactの宣言的なデータフローをハックする禁じ手」でもある。何でもかんでも `forwardRef` でDOM操作や命令型コードを書くのは、Reactの設計思想(状態駆動)に逆行するため、コードベースがスパゲッティ化する原因になる。

基本はあくまで「PropsとStateでデータを渡し、UIをリアクティブに描画する」。
その上で、
1. フォーカスの制御
2. テキストの選択やスクロール位置の調整
3. 外部のサードパーティライブラリ(D3.jsやCanvasなど)との統合

こうした、「どうしても命令型(Imperative)にDOMを操作しなければならない絶対領域」においてのみ、誇りを持って `forwardRef` を取り出してほしい。

型定義の作法と、コンポーネントのカプセル化の意識。この2つさえ押さえておけば、君が書くコンポーネントはチーム内で一目置かれる「信頼できるコード」になるはずだ。さあ、エディタに戻って実装を始めようか。

コメント

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