はじめに
実務でまたしても何をしているのかわからないコードに出会いました。
初めましてのforwardRef
です。
この記事では、実務で出てきたこちらのコードの意味を理解することをゴールとします。また調べる中で生まれた疑問を元に、forwardRefをいつ使うべきかについての推測も記載します。
import { ComponentProps,forwardRef } from 'react'
export type TextInputProps = ComponentProps<'input'>
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>((props, ref) => {
return <input ref={ref} {...props} />
})
先に結論を書きます。
- 自分で定義したコンポーネントにはrefを渡せない
-
forwordRef
を使うと、自分で定義したコンポーネントにもrefを渡せるようになる。これにより、親コンポーネントから子コンポーネントのDOM要素を操作できるようになる
自分で定義したコンポーネントにrefを渡せないらしい
React公式ドキュメントを調べてみました。
<input />
のようなブラウザ要素を出力する組み込みコンポーネントに ref を置いた場合、React はその ref のcurrent
プロパティを、対応する DOM ノード(ブラウザの実際の<input />
など)にセットします。
例として「テキスト入力フィールドにフォーカスを当てる」が公式に挙げられています。
refでDOMを操作するのは何度か経験しました。
問題は、別のコンポーネントのDOMノードにアクセスする場合です。
ただし、独自のコンポーネント、例えば
<MyInput />
に ref を置こうとすると、デフォルトではnull
が返されます。
デフォルトでは React は、コンポーネントが他のコンポーネントの DOM ノードにアクセスできないようにしているためです。自分自身の子でさえもです!
例として挙げられているソースコードで、コンソールにWarningが出ているのが確認できます。
<MyInput ref={inputRef} />
がダメってことみたいですね。
Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
ボタンをクリックしてhandleClickを実行してみると以下のエラーが発生しました。
App.js: Cannot read properties of null (reading 'focus')
inputRef.current
がnullだからfocusできないよーと言われているようです。
別のコンポーネントの DOM ノードまで手動で操作できてしまうと、コードがさらに壊れやすくなってしまいます。
Reactがあえてこの仕様にしているようです。
Reactはrefを「避難ハッチ」としています。こんな風に書くということは、あんまり使ってほしくないんでしょうね🧐
forwordRefを使うと自分で定義したコンポーネントにrefを渡せる
別のコンポーネントからrefを渡してDOM操作をしたい場合もありますよね。そこで使われるのが、forwordRef
です。
代わりに、内部の DOM ノードを意図的に公開したいコンポーネントは、そのことを明示的に許可する必要があります。コンポーネントは、自身が受け取った ref を子のいずれかに「転送 (forward)」するよう指定できます。
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
forwardRef
を使うと、親コンポーネントから子コンポーネントの要素にrefを渡せるようになります。
無制限に全てのコンポーネントのDOM要素を操作できたら、どこの何の要素なのか分からなくなって混乱しそうです。forwardRef
を使う場合は、forwardRef
で公開した特定のDOM要素だけ、他のコンポーネントからアクセスできるようになるから安全でしょ??的なニュアンスかなと思いました。
forwordRefに型をつける
TypeScriptで型をつけると以下のようになります。
React TypeScript Cheatsheetの例を載せます。
import { forwardRef, ReactNode } from "react";
interface Props {
children?: ReactNode;
type: "submit" | "button";
}
export type Ref = HTMLButtonElement;
export const FancyButton = forwardRef<Ref, Props>((props, ref) => (
<button ref={ref} className="MyClassName" type={props.type}>
{props.children}
</button>
));
forwordRef
はジェネリクスで型を指定します。
forwardRef<RefType, PropsType>
- RefType: 転送するrefの型(この場合Ref(=
HTMLButtonElement
)) - PropsType: コンポーネントが受け取るpropsの型(この場合
Props
)
RefTypeを指定することで、このコンポーネントに渡せるrefの型をHTMLButtonElement
に限定します。これにより、<div>
や<input>
などの他の要素のrefを誤って渡すことを防ぐことができます。
これでより安全にコンポーネント間でrefを受け渡すことができます。
わからなかったコードを振り返る
ここまで調べれば、実務でわからなかったコードを解読できそうです。
import { ComponentProps,forwardRef } from 'react'
export type TextInputProps = ComponentProps<'input'>
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>((props, ref) => {
return <input ref={ref} {...props} />
})
<TextInput>
コンポーネントはforwardRef
を使用して定義されています。これにより、親コンポーネントからrefを受け取り、内部の要素に転送できます。
これにより、親コンポーネントからDOM操作ができるようになるというわけですね。
例えばこんな感じで、App.tsx
で定義したrefを<TextInput>
コンポーネントに渡します。Focus the inputをクリックするとhandleClick
が実行され、<input>
要素にフォーカスされます。
疑問
forwardRef
を調べて、わからなかったコードの意味は理解できたのですが疑問が生まれました。
「私今まで何度もrefをpropsで渡してきたよな?🧐」 と。
調べてみると、「自分で定義したコンポーネントにrefを渡せない」のではなく「自分で定義したコンポーネントにrefという名前でpropsを渡せないらしい」です。
例えば、以下のような場合エラーは起きず、問題なく動きます。
import React, { useRef, RefObject } from 'react';
type TextInputProps = ComponentProps<'input'> & {
inputRef: RefObject<HTMLInputElement>;
};
// <TextInput>は`inputRef`という名前でrefをpropsとして受け取る
function TextInput({ inputRef, ...props }: TextInputProps) {
return <input {...props} ref={inputRef} />;
}
export default function MyForm() {
const inputRef = useRef<HTMLInputElement>(null);
function handleClick() {
inputRef.current?.focus();
}
return (
<>
{/* 親コンポーネントは定義したrefを`inputRef`という名前のpropsで渡す */}
<TextInput inputRef={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
forwardRefを使わなくてもrefを渡せてしまっていますよね。
propsの名前を変えるだけでrefを渡せるなら、forwardRefいらないんじゃないか???
いつforwardRefを使うか考えてみる
ここからは私の推測です。(間違っている可能性があります🙇)
propsの名前を変えるだけでrefを渡せる仕様から、Reactでは通常のpropsとしてrefを渡すことを技術的には禁止していないことがわかります。
別のコンポーネントからDOM操作させたくないReactが、refをpropsで渡す仕様をあえて残したのは、refがDOM操作をするためだけに使われるものではないからだと思います。
refは値を参照することにも使われます。
コンポーネントに情報を「記憶」させたいが、その情報が新しいレンダーをトリガしないようにしたい場合、ref を使うことができます。
例えば、前回の状態やタイマーIDなど、再レンダーに影響を与えない情報を保持できます。
このときstateのように、propsとして複数のrefに名前をつけて別コンポーネントに渡す可能性もあります。どんな場合でもpropsとしてrefを渡すことを禁止してしまったら、困る人が急増しそうです。
単純な値を参照したいときにpropsとしてrefを渡せるようにこの仕様を残したのではないでしょうか。
Reactコンポーネントにはrefをわざわざpropsで定義しなくても渡せるようになっているので、内部的に特別なpropsとして扱われていると思われます。
ユーザーがrefという名前で、Reactが管理するDOM要素へアクセスするようなrefをpropsで渡せてしまうとしたら、内部実装とぶつかってしまう気がします。
よって、単純な値を参照したいときはforwardRef
を使わずにpropsとしてrefという名前以外でrefを渡し、DOM操作をしたいときはforwardRef
を使ってrefを転送するという使い分けがいいのではないかと思いました。
まとめるとこんなイメージ。
-
forwardRefを使うとき
親コンポーネントから子コンポーネントの内部DOM要素を操作したいとき -
propsとしてrefを渡すとき
親コンポーネントから子コンポーネントへ、値の参照や状態の保持など、再レンダーに影響を与えない情報を渡したいとき。refという名前を避けて他の名前で渡す
終わりに
わからなかったソースコードの解読をするためにforwardRef
を調べたら、疑問が生まれてしまいました…
いつforwardRefを使うか考えてみるについては、私の推測なので間違っている可能性があります🙇
補足や指摘事項等ありましたらコメントいただけると嬉しいです!!