1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

実務でまたしても何をしているのかわからないコードに出会いました。
初めましてのforwardRefです。

この記事では、実務で出てきたこちらのコードの意味を理解することをゴールとします。また調べる中で生まれた疑問を元に、forwardRefをいつ使うべきかについての推測も記載します。

TextInput.tsx
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を受け渡すことができます。

わからなかったコードを振り返る

ここまで調べれば、実務でわからなかったコードを解読できそうです。

TextInput.tsx
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を使うか考えてみるについては、私の推測なので間違っている可能性があります🙇
補足や指摘事項等ありましたらコメントいただけると嬉しいです!!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?