コンポーネント内の 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" />; };