コンポーネント内の DOM 要素にRefオブジェクトを渡すための機能であるforwardRefと、コンポーネントにメソッドを生やす Hooks であるuseImperativeHandleについて、説明する。
どちらもRefオブジェクトやref属性を使った機能なので、それらを理解していることが前提になる。
理解が不十分な場合はまず Ref の基本を学ぶことをおすすめする。
この記事のコードは React のv16.10.2で動作確認している。
forwardRef
forwardRefを学ぶための題材として、テキストボックスへのフォーカスを扱う。
以下のコードでは、focusボタンを押すとテキストボックスにフォーカスする。
import React, {useRef} from 'react'; const App = () => { const ref = useRef(); const onClick = () => { ref.current.focus(); }; return ( <div> <input ref={ref} type="text" /> <br /> <button type="button" onClick={onClick}> focus </button> </div> ); }; export default App;
Refオブジェクトをinput要素のref属性に渡すことで、input要素の操作(この例ではフォーカス)が可能になる。
では以下のように、input要素がApp直下の要素ではなく、子コンポーネントの要素だった場合は、どうすればいいのか。
import React from 'react'; const MyInput = () => { return <input type="text" />; }; const App = () => { return ( <div> <MyInput /> </div> ); }; export default App;
<MyInput ref={ref} />のようにMyInputがpropsとしてrefを受け取り、さらにそれをinputのref属性に渡す、ということは出来ない。
refは特殊な属性で、propsには含まれないからだ。そもそも、関数コンポーネントにrefを渡すことは出来ない。
そこで、forwardRefを使う。これを使うことで、関数コンポーネントにrefを渡すことができ、さらにそのなかで自由にrefを使うことが出来る。
forwardRefは関数を返す関数で、引数として関数コンポーネントを渡すと、そのコンポーネントに機能を追加した新しいコンポーネントを返す。
その追加機能によって、関数コンポーネントにref属性を設定できるようになり、設定されたrefは関数コンポーネントの第二引数として受け取る。
以下の例では、forwardRefにMyInputを渡して、MyInputをベースとしたWrappedMyInputという新しい関数コンポーネントを作成している。
そして<WrappedMyInput ref={ref} />とすることで、そのコンポーネントは第二引数としてrefを受け取るようになる。
import React, {useRef} from 'react'; const MyInput = (props, ref) => { console.log(ref); // {current: undefined} return <input type="text" />; }; const WrappedMyInput = React.forwardRef(MyInput); const App = () => { const ref = useRef(); return ( <div> <WrappedMyInput ref={ref} /> </div> ); }; export default App;
この機能を利用することで、App側から子コンポーネント内の DOM 要素を操作することが出来る。
import React, {useRef} from 'react'; const MyInput = (props, ref) => { return <input ref={ref} type="text" />; }; const WrappedMyInput = React.forwardRef(MyInput); const App = () => { const ref = useRef(); const onClick = () => { ref.current.focus(); }; return ( <div> <WrappedMyInput ref={ref} /> <br /> <button type="button" onClick={onClick}> focus </button> </div> ); }; export default App;
さて、ここまで書いてきたが、実はforwardRefを使わなくても、同等のことは出来る。
refは特殊な属性だと書いたが、裏を返せば、refとは異なる別の名前でRefオブジェクトを渡してしまえば、それで済む。
それがこのサンプル。
import React, {useRef} from 'react'; const MyInput = props => { return <input ref={props.customRef} type="text" />; }; const App = () => { const ref = useRef(); const onClick = () => { ref.current.focus(); }; return ( <div> <MyInput customRef={ref} /> <br /> <button type="button" onClick={onClick}> focus </button> </div> ); }; export default App;
customRefという属性を用意し、それを通してpropsのバケツリレーを行うことで、問題なくinput要素にRefオブジェクトを渡せている。
そのため、forwardRefの必要性が、私にはよく分からない。
各々が独自の属性を用意するのではなく、Refオブジェクトを渡す属性はrefに固定してしまう、ということが可能になるのが、利点なのかもしれない。
useImperativeHandle
useImperativeHandleは Hooks のひとつで、関数コンポーネントにメソッドを追加し、それを親コンポーネントから使えるようにするための機能。
言葉で説明するとイメージしづらいが、サンプルを見れば、大して難しい話でもないということが分かると思う。
説明に入る前に断っておくと、他のRefに関する機能と同様、useImperativeHandleも基本的には使用を避けるべき。
公式ドキュメントにも、以下のように書かれている。
いつもの話ですが、ref を使った手続き的なコードはほとんどの場合に避けるべきです
あくまでもこのような機能もある、という話であり、実際に使うケースはほとんどない。
useImperativeHandleを使うにはまず、Refオブジェクトを関数コンポーネントに渡す必要があるので、forwardRefを使うことにする。
そして関数コンポーネントのなかで、以下のようにuseImperativeHandleを使う。使う場所は、他の Hooks と同様に、関数コンポーネントのトップレベル。
useImperativeHandle(親コンポーネントから受け取った`Ref`オブジェクト, () => ({ // ここに、追加したいメソッドを書いていく }));
具体例として、MyInputにlogというメソッドを持たせた。
import React, {useRef, useImperativeHandle} from 'react'; const MyInput = (props, ref) => { useImperativeHandle(ref, () => ({ log: () => { console.log('called log'); }, })); return <input type="text" />; }; const WrappedMyInput = React.forwardRef(MyInput); const App = () => { const ref = useRef(); const onClick = () => { ref.current.log(); // called log }; return ( <div> <WrappedMyInput ref={ref} /> <br /> <button type="button" onClick={onClick}> click </button> </div> ); }; export default App;
こうするとApp側でref.current.logを実行できるようになる。
これも、forwardRefを使わずに書くことが可能。
const MyInput = props => { useImperativeHandle(props.customRef, () => ({ log: () => { console.log('called log'); }, })); return <input type="text" />; };
useImperativeHandle の依存配列
useImperativeHandleは第三引数に依存配列を渡すことが出来る。
依存配列については以下の記事に詳しく書いたが、これを正しく使うことで、同じ内容のメソッドを何度も作り直してしまうことを防げる。
まずは依存配列を使わないパターン。
ボタンを押す度にstateが更新されるため再レンダーが発生し、useEffectが実行される。
その度にMyInputも再レンダーされるため、都度useImperativeHandleが実行される。
そうするとsomeMethodも新しく作り直されるため、Xは常にfalseとなる。
import React, {useRef, useImperativeHandle, useEffect, useState} from 'react'; const MyInput = (props, ref) => { useImperativeHandle(ref, () => ({ someMethod: () => {}, })); return <input type="text" />; }; const WrappedMyInput = React.forwardRef(MyInput); const list = []; const App = () => { const ref = useRef(); const [state, setState] = useState(0); useEffect(() => { list.push(ref.current.someMethod); if (list.length >= 2) { console.log(list[list.length - 2] === list[list.length - 1]); // X } }); const onClick = () => { setState(s => s + 1); }; return ( <div> <WrappedMyInput ref={ref} attr={state} /> <br /> <button type="button" onClick={onClick}> click </button> </div> ); }; export default App;
MyInputを以下のように書き換えると、useImperativeHandleは初回レンダー時にしか実行されないため、someMethodは同じ参照を指し続ける。
そのため、Xは常にtrueになる。
const MyInput = (props, ref) => { useImperativeHandle( ref, () => ({ someMethod: () => {}, }), [] ); return <input type="text" />; };