refとは?
refとはReference(参照)のことです。ミュータブルな値を管理するときに使います。
refの使いみちの最たるものはDOMへのアクセスです。たとえばinput要素が持つfocusメソッドを呼びたいときにrefを使います。
const App = () => { const ref = useRef(); useEffect(() => ref.current.focus(), []); // レンダー後、inputにフォーカスする return <input ref={ref}/>; };
上記の例では、refはDOMへの参照となっています。もちろん、refを使えばinputのvalueを変更したり、focusする以外にも様々なことを命令的にできます。しかし、DOMを命令的に操作するのはReactのやり方に相反します。そのため、できる限りrefを使わず宣言的に書く方法を考えるべきです。
refは単にミュータブルなオブジェクトです。そのため、生DOMへのアクセス以外にも使うことができます。ただし、そこまで用途はありません。というより、他の手法で十分なことがほとんどです。refを使いたくなったら一旦落ち着いて他の方法を考えましょう。
forwardRef
通常、関数コンポーネントはrefを受け取ることができません。たとえば下のMyInputのようなことはできません。
// ダメな例(関数コンポーネントはrefを受け取れない) const MyInput = ({ref}) => ( <input ref={ref}/> ); const App = () => { const ref = useRef(); const onClick = () => { ref.current.focus() }; // MyInputにrefは渡せない! return ( <div> <MyInput ref={ref}/> <button onClick={onClick}>focus</button> </div> ); }
refは特殊なpropなので、そのままでは受け取れません。forwardRefを使ってやると関数コンポーネントでもrefを受け取れます。
// forwardRefを使えば関数コンポーネントでもrefを受け取れる const MyInput = React.forwardRef((prop, ref) => ( <input ref={ref}/> ));
String refを使っていたコンポーネントをhooksで書き直す
useImperativeHandleを使うと、string refを使っていたクラスコンポーネントを関数コンポーネントへ書き直すことができます。
以下のChildコンポーネントはstring refを使って複数のinputへのrefを親コンポーネントへ提供しています。
// string refはclass componentでしか扱えない class Child extends React.Component { render() { return ( <form> <input type="email" ref="email"/> <input type="password" ref="password"/> <input type="text" ref="username"/> </form> ); } } class App extends React.Component { focusOnEmail() { // Child内のinput[type=email]へアクセスできる this.refs.form.refs.email.focus(); } render() { return ( <div> <Child ref="form"/> <button onClick={this.focusOnEmail.bind(this)}>focus on email</button> </div> ) } }
Childコンポーネントをhooksを用いて関数コンポーネントへ書き直します。
const Child = React.forwardRef((prop, ref) => { const emailRef = useRef(), passwordRef = useRef(), usernameRef = useRef(); useImperativeHandle(ref, () => { return { refs: { get email() { return emailRef.current; }, get password() { return passwordRef.current; }, get username() { return usernameRef.current; }, } }; }) return ( <form> <input type="email" ref={emailRef}/> <input type="password" ref={passwordRef}/> <input type="text" ref={usernameRef}/> </form> ); });
これで従来のstring refでできたことが再現できます。
(型をつけるのが非常に困難なので、こんな方法するくらいなら、素直にリファクタリングしましょう)
そもそも
ただし、そもそもこれはstring refからhooksへ置き換えられるだけです。古いクラスコンポーネントを書き直すとき以外、こんな方法しなくて十分です。というのも幾つか理由があります。
- Appコンポーネント側からrefs以下に何があるか分からない
- 下のコンポーネントでhookを使いたくない
そのため、個人的にこの方法は好きではありません。
いちから作るのであれば、まずApp側でrefを作り、Childはそれを受け取って紐付けるだけにするとシンプルになります。以下はChildをSFCにしてrefを受け取る例です。
const Child = ({ emailRef, passwordRef, usernameRef }) => ( // useImperativeHandleのようなことをする必要がない! <form> <input type="email" ref={emailRef} /> <input type="password" ref={passwordRef} /> <input type="text" ref={usernameRef} /> </form> ); const App = () => { const emailRef = useRef(), passwordRef = useRef(), usernameRef = useRef(); // どういうrefがあるのかApp側が明示的に持てる const focusOnEmail = () => emailRef.current.focus(); return ( <div> <Child emailRef={emailRef} passwordRef={passwordRef} usernameRef={usernameRef} /> <button onClick={focusOnEmail}>focus on email</button> </div> ); };
TypeScriptでの型付け的にもこちらのほうが筋が通っています。
もちろん、下のほうの実装を隠蔽したいときはuseImperativeHandleが活躍します。