ReactのRefを理解したい
割と雰囲気で使っていたのでちゃんと理解したいと思います。
Refとは
基本的なRefに関しての話と、useRefを使った話の二段構成になります。まずは基本的な話から。
公式ドキュメントを読んでみましょう。
基本的にはあまりRefに頼らないようにしましょう、ということが随所に書かれています。
コンポーネントに親子関係がある場合、基本的に子コンポーネントの詳細は隠蔽されていますが、Refを利用すると親が子の詳細を知らなくてはならない=依存関係が生まれてしまうためですね。
Refの概要としては以下のような感じです。
- React.createRef()で生成する
- クラスコンポーネントで使うもので、コンポーネントの構築時にインスタンスプロパティに割り当てられる
- 関数コンポーネントに対しては、ひと手間加えないとRefを使用することはできない
- フォワーディングを使用すると子コンポーネントのrefを公開することができる
具体的な使用例としては以下の内容が紹介されています。
- フォーカス、テキストの選択およびメディアの再生の管理
- アニメーションの発火
- サードパーティの DOM ライブラリとの統合
https://ja.reactjs.org/docs/refs-and-the-dom.html#when-to-use-refs より
業務でもフォーカスに関する機能の実装の際に活用した記憶があります。公式ドキュメント内の例でもinputへのフォーカスが紹介されていますね。
さて、実際に動かしてみましょうか。
使ってみる
ここではViteを使ってプロジェクトを作成しますが、お好みの方法でReactが動作する環境を作ってください。
yarn create vite learn-ref --template react-ts
cd learn-ref
yarn
yarn dev
これで localhost:3000 でReactが動きます。
そしたら App.tsx
を以下の内容に書き換えましょう。
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return (
<div ref={this.myRef}>
<button
onClick={() => {
console.log(this.myRef);
}}
>
button
</button>
</div>
);
}
}
export default App;
画面に表示されるボタンをクリックすると、コンソールにRefの内容が表示されます。こんな感じになっていることでしょう。
Object { current: div }
これはドキュメントにある、
HTML 要素に対して ref 属性が使用されている場合、React.createRef() を使ってコンストラクタ内で作成された ref は、その current プロパティとして根底にある DOM 要素を受け取ります
https://ja.reactjs.org/docs/refs-and-the-dom.html#accessing-refs
こちらの挙動ということでしょう。
ついでにカスタムコンポーネントに関する挙動も見ておきましょうか。
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.customRef = React.createRef();
}
render() {
return (
<div>
<button
onClick={() => {
console.log(this.customRef);
}}
>
button
</button>
<CustomComponent ref={this.customRef} />
</div>
);
}
}
class CustomComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return <p>this is custom component</p>;
}
}
export default App;
この場合は、currentに CustomComponent
が入ったオブジェクトが表示されます。
例に倣い、
ref 属性がカスタムクラスコンポーネントで使用されるとき、ref オブジェクトはコンポーネントのマウントされたインスタンスを current として受け取ります
https://ja.reactjs.org/docs/refs-and-the-dom.html#accessing-refs
こちらの挙動が確認できるはずです。
さて、DOMが取得できることがわかったので、操作を加えてみましょうか。
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return (
<div>
<button
onClick={() => {
this.myRef.current.focus();
}}
>
focus
</button>
<button
onClick={() => {
this.myRef.current.blur();
}}
>
blur
</button>
<button
onClick={() => {
this.myRef.current.value = "filled";
}}
>
fill
</button>
<input type="text" ref={this.myRef} />
</div>
);
}
}
export default App;
各ボタンを押すとinputを操作できるようになりました。
Refの基本はこんなところでしょうか。続けて、関数コンポーネントにおけるRefの活用について見ていきたいと思います。
useRef / useImperativeHandle
createRefはクラスコンポーネントでしか扱えません。関数コンポーネントで活用するためには useRef
を使います。
先ほどの例を関数コンポーネントに書き直すとこうなります。
import React, { useRef } from "react";
const App = () => {
const ref = useRef();
return (
<div>
<button
onClick={() => {
ref.current.focus();
}}
>
focus
</button>
<button
onClick={() => {
ref.current.blur();
}}
>
blur
</button>
<button
onClick={() => {
ref.current.value = "filled";
}}
>
fill
</button>
<input type="text" ref={ref} />
</div>
);
};
export default App;
ここで2つのref活用法を紹介しましょう。
書き換え可能な値を保持する
本質的に useRef とは、書き換え可能な値を .current プロパティ内に保持することができる「箱」のようなものです。
実用性があるかは微妙ですが、コンポーネントごとにID(この例ではtimestampを使います)を持たせたいというケースを考えてみましょう。
何も考えずに書くとこうです。
import React, { useState, useRef } from "react";
const App = () => {
const [state, setstate] = useState("");
return (
<div>
<input
type="text"
value={state}
onInput={(e) => setstate(e.target.value)}
/>
<ChildComponent />
</div>
);
};
const ChildComponent = () => {
const id = new Date().getTime();
return (
<div>
<p>{id}</p>
</div>
);
};
export default App;
親コンポーネントのstateが更新されるたびに再描画が発生し、ChildComponentのIDが変わってしまいますね。
useRefを使って改善するとこうなります。
const ChildComponent = () => {
const id = useRef(new Date().getTime());
return (
<div>
<p>{id.current}</p>
</div>
);
};
useRef() を使うことと自分で {current: ...} というオブジェクトを作成することとの唯一の違いとは、useRef は毎回のレンダーで同じ ref オブジェクトを返す、ということです。
と記載がある通り、はじめて生成したときと同じオブジェクトを返してくれるため、再描画が走ってもIDが変わることはありません。
注記で言っているのは、以下のような実装だとidに代入しているオブジェクトは描画の度に生成されるのでだめですよ、ということですね。
const ChildComponent = () => {
const id = {
current: new Date().getTime(),
};
return (
<div>
<p>{id.current}</p>
</div>
);
};
親から触る
inputに対する操作の例では同一コンポーネント内に対象があったため普通に使えていましたが、子コンポーネント内のコンポーネントを対象とする場合について見てみましょう。
この場合はforwardRefというのが必要になります。
import React, { useState, useRef, forwardRef } from "react";
const App = () => {
const ref = useRef();
return (
<div>
<button
onClick={() => {
console.log(ref.current.value);
}}
>
fill
</button>
<ChildComponent ref={ref} />
</div>
);
};
const ChildComponent = forwardRef((props, ref) => {
return (
<div>
<input type="text" ref={ref} />
</div>
);
});
export default App;
ChildComponentで使ってるやつですね。
また、子コンポーネントで定義されている内容を親に共有することもできます。useImperativeHandleというやつを使います。
Child側に、自身にフォーカスさせる関数を定義して、親から呼べるようにしてみましょう。
import React, { useRef, forwardRef, useImperativeHandle } from "react";
const App = () => {
const ref = useRef();
return (
<div>
<button
onClick={() => {
ref.current.focus();
}}
>
focus
</button>
<ChildComponent ref={ref} />
</div>
);
};
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
}));
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
});
export default App;
こんな具合です。親コンポーネントから子コンポーネントで定義した処理を呼ぶことができました。
たいていはpropsを工夫したり設計を見直したりすることによって、「Refを使わないと実現できない!」とならないこともあり推奨されていないのだと思いますが、いざという時のための手段として覚えておくとよいですね。
Discussion