Manipulating the DOM with Refs
前回の続きです。
今回は ref
による DOM操作に関する記事です。
ref
がよく使われるケース
- ノードにフォーカスする、ノードまでスクロールする、ノードのサイズやポジションを測定するなど
使い方
<div ref={myRef}>
のようにしてDOMに ref
を移植するだけです。
これでDOMに対して 以下のようなAPI を使用して操作が可能
例えば、myRef.current.scrollIntoView()
でそのノードまでスクロールできます。
自作のコンポーネントに ref
を使う場合
<input ref={inputRef} />
のような一般的なDOMに対してブラウザから操作する要素にrefを指定するのは問題ありません。
一方で自分で作ったコンポーネントの場合、下記の例のように何も考えずに <MyInput ref={inputRef} />
のようにすると inputRef.current.focus()
はエラーになります。
import { useRef } from 'react';
function MyInput(props) { // ここで refを受け取れていない
return <input {...props} />; // このDOMに ref を渡さないといけない
}
export default function MyForm() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus(); // エラーになる
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
Focus the input
</button>
</>
);
}
原因は<myInput />
が <input>
をラップしていて、この状態では ref
を子に渡せていないからです。
デフォルトでは子コンポーネントにref
を渡すようになっていません。
そうした場合は、下記のようにforwardRef()
を使って記述することで子に ref
を渡すことができ、これによって操作が可能になります。
const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});
ref
の操作を制限する
また、子に渡したref
を使うと、さまざまなAPIが使えてしまいますが、以下のように
useImperativeHandle()
を使うことで、ref
の操作を限定することができます。
下の例では使用できるAPIをfocus()
のみに限定しています。
const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// Only expose focus and nothing else
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});
ステートと ref
の変更が反映されるまでの時間の違い
ステートの変更は待ち行列で処理され、即時に評価されません。
一方で ref
はすぐに評価されるため、ステートの処理より先に評価されてしまいます。
そのため、以下のようなコードは一番最後の要素にスクロールされません。
setTodos([ ...todos, newTodo]);
listRef.current.lastChild.scrollIntoView();
処理されるタイミングを合わせるには
上記のような問題に対処するには flushSync
を使います。
ステートの変更を待つように指定することができるのです。
以下の例ではコードは一番最後の要素にスクロールできます。
import flushSync from react-domして、
flushSync(() => {
setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
ref
の注意点
Reactで管理されているDOMに ref
の使用はできるだけ避けた方がよいそうです。
特に子要素を持つnodeには注意。
しかし、全く操作してはいけないというわけではないそうです。