ReactのRefとRefForwardingを一気に学び直した
動機
業務でstyled-componentsを触る機会がありました。
styled-componentsは自分の周りではあまり良い噂を聞いていなかったため、少し調べたところこんな記事がHitしました。
記事の内容としては
- 様々な基礎概念を隠蔽しすぎている
- 使うことのメリットデメリットや、隠蔽されている部分の理解をしたチームでないと使わないほうが良い
という内容です。
なので、しっかりと順を追って復習しました。
環境
viteで作成したReact18のTypeScriptプロジェクトで実施。
Reactの利用は関数型コンポーネントを前提としています。
Refを軽くおさらい
公式Docを見ていただくのが一番正確です。
公式では
一般的な React のデータフローでは、props が、親コンポーネントがその子要素とやりとりする唯一の方法です。子要素を変更するには、新しい props でそれを再レンダーします。ただし、この一般的なデータフロー以外で、子要素を命令型のコードを使って変更する必要がある場合もあります。変更したい子要素が React コンポーネントのインスタンスのことも、DOM 要素のこともあるでしょう。どちらの場合でも、React は避難ハッチを提供します。
小難しいのですが、宣言的であるReactで手続き的にDOMを操作する際にしばしば利用されます。(厳密にはDOM操作だけのためにあるのものではないのですが割愛します。※インスタンス変数のようなものはありますか?)
実際の利用イメージとしては、jQueryの手続き的にDOMを操作していく方法がとても近いです。
Refはいつ使うのか
公式が提示しているユースケースとしてもDOMへの直接的な操作を挙げています。
- フォーカス、テキストの選択およびメディアの再生の管理
- アニメーションの発火
- サードパーティの DOM ライブラリとの統合
私が身近であると感じるのは以下の2点です。
- サードパーティの DOM ライブラリとの統合
- atomsのような非制御コンポーネントにとても近いコンポーネントの作成
atomsの非制御/制御についてはこちらの記事が大変分かりやすいです。
ハンズオン
Refの挙動のチェックから、RefForwardingにまで一気に触れていきたいと思います。
作ったコンポーネントにRefを渡してみる
こんなatomsのコンポーネントを用意してみました。
ポイントはこの時点では非制御コンポーネントであるbutton
にrefを渡していない部分です。
import React, { ComponentProps } from "react";
type Props = ComponentProps<"button">;
export const Button: React.FC<Props> = ({ children }) => {
return <button>{children}</button>;
};
作成したButtonコンポーネントへRefを渡してDOMにアクセスしてみます。
import { useEffect, useRef } from "react";
import "./App.css";
import { Button } from "./components/core/Button";
function App() {
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (buttonRef.current) { // ①
console.log(buttonRef.current);
buttonRef.current.disabled = true;
} else {
console.log("ref is null"); // ②
}
},[]);
return (
<div className="App">
<div>
<h1>Hello</h1>
<Button ref={buttonRef}>Click Me</Button>
</div>
</div>
);
}
export default App;
- DOMへアクセスできたら、buttonの状態をdisabledへ
手続き的
に変更する - アクセスできない場合、consoleに
ref is null
と表示する。
結果はRefへのアクセスに失敗し、分岐はnullになります。
理由としてはRefを受け取っても、実際に返す非制御コンポーネントに渡されていないのが問題です。
import React, { ComponentProps } from "react";
type Props = ComponentProps<"button">;
export const Button: React.FC<Props> = ({ children }) => {
return <button>{children}</button>; //ここにrefが渡されていない!
};
またエラーの内容にReact.forwardRef()
がありますが、あとで触れますので一旦飛ばします。
PropsでRefを取り出して渡してみる
import React, { ComponentProps } from "react";
type Props = ComponentProps<"button">;
export const Button: React.FC<Props> = ({ children, ref }) => {
return <button ref={ref}>{children}</button>;
};
エラーが変わって
ref
is not a prop. Trying to access it will result inundefined
being returned. If you need to access the same value within the child component, you should pass it as a different prop.
refはpropではないという重要事実が発覚します。
もしpropで渡すのなら、別の(別変数に入れた)propとして渡してくださいとのこと。
独自のProps定義をしてRefを渡してみる
指示通り、myRef
を定義して渡してみます。
import React, { ComponentProps } from "react";
type Props = ComponentProps<"button"> & {
myRef?: React.RefObject<HTMLButtonElement>;
};
export const Button: React.FC<Props> = ({ children, myRef }) => {
return <button ref={myRef}>{children}</button>;
};
意図したとおりにconsoleにdomが表示され、ボタンもdisabledになりました。
myRef
ではなくもっと普通に非制御コンポーネントのようにrefを渡せないのか?
ここでRefの転送(Forwarding Ref という考え方、方法)が出てきます。
forwardRef関数
(引数にFCを受け取る高階関数)を使って、制御コンポーネントが渡されたrefオブジェクトを受け取る(転送)
することができる
import React, { ComponentProps, forwardRef } from "react";
type Props = ComponentProps<"button">;
export const Button = forwardRef<HTMLButtonElement, Props>(
({ children }, ref) => {
return <button ref={ref}>{children}</button>;
}
);
もちろん利用側も非制御コンポーネントを扱うかのようにRefを渡せる
import { useEffect, useRef } from "react";
import "./App.css";
import { Button } from "./components/core/Button";
function App() {
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (buttonRef.current) {
console.log(buttonRef.current);
buttonRef.current.disabled = true;
} else {
console.log("ref is null");
}
},[]);
return (
<div className="App">
<div>
<h1>Hello</h1>
<Button ref={buttonRef}>Click Me</Button> //違和感ないね!
</div>
</div>
);
}
export default App;
一応こういった議論もあるようで、ComponentPropsWithRefなどを利用し厳密に型を定義したほうが良さそうです。
import React, { forwardRef } from "react";
type Props = React.ComponentPropsWithRef<"button">;
export const Button: React.FC<Props> = forwardRef<HTMLButtonElement, Props>(
({ children }, ref) => {
return <button ref={ref}>{children}</button>;
}
);
最後に
styled-componentsの利用から、隠蔽されているref、refの転送の理解などをする動機が得られたことを、冒頭の記事のおかげでできたため、作者であるhs様には感謝しています。
Discussion