Dom参照以外でReactのuseRefを使う
初めに(背景)
最近お仕事でReactを使い始め(ずっとVue.jsやってました)、色々と知見が溜まってきたので投稿していきます。
今日はReact hooksのuseRefについてです。
今までReact使ってきて、useRefはdomへの参照をしたい場合に使うものだと思っていましたが、実はそれだけだけではなかったということを最近学びましたので復習をかねて書き綴っていきます。
useRefの説明
https://ja.reactjs.org/docs/hooks-reference.html#useref
本家Reactのドキュメントにも書いてある通り、よくあるユースケースはDOMへのアクセスです。
しかし、記事を下の方に読み進めていくとこうあります。
しかしながら useRef() は ref 属性で使うだけではなく、より便利に使えます。これはクラスでインスタンス変数を使うのと同様にして、あらゆる書き換え可能な値を保持しておくのに便利です。
そう、あらゆる書き換え可能な値を保持するのに使えるらしいです。
これだけだとわけわかめだと思うので、実際のユースケースを紹介します。
domのref以外で使う場合のユースケース
自由に追加、削除できる以下のようなListについて考えてみましょう。
function List (items, append, remove) {
return (
<>
<ul style={{ maxHeight: '500px', overflowY: 'scroll' }}>
{
items.map((item) => {
return (
<li key={item.id}>
{item.content}
<button onClick={() => remove(item.id)}>
delete
</button>
</li>
)
})
}
<ul/>
<button onClick={append}>add</button>
</>
)
}
このコンポーネントの機能要件は以下とします。
1) ある程度の行が長くなったときのために`overflow-y`でul内をスクロールできるようする
2) 行が追加された時、追加された行の場所(一番最後)までスクロールしたい
3) 行を削除したときは何もスクロールしない
すでに1については実装できているので、2と3について考えてみます。
どちらもpropsで渡されるitemsの長さを監視して、増えたらスクロール、同じか減ったら何もしないという操作が必要になりそうです。
それを踏まえ、以下のように実装してみました。
function List (items, append, remove) {
const [prevItemsLen, setPrevItemsLen] = useState(items.length);
const scrollAreaRef = useRef(null);
const itemsLen = items.length;useEffect(() => {
if (itemsLen > prevItemsLen && scrollAreaRef) {
scrollAreaRef.current.scrollTop
= scrollAreaRef.current.scrollHeight;
}
setPrevItemsLen(itemsLen)
}, [itemsLen, prevItemsLen, setPrevItemsLen]);return (
<>
<ul
ref={scrollAreaRef}
style={{ maxHeight: '500px', overflowY: 'scroll' }}
>
{
items.map((item) => {
return (
<li key={item.id}>
{item.content}
<button onClick={() => remove(item.id)}>
delete
</button>
</li>
)
})
}
<ul/>
<button onClick={append}>add</button>
</>
)
}
増減の変化を観測するのにitemsの1つ前の世代の長さが必要なため、prevItemsLenというstateを用意しました。
このコードは確かに動くはずですが、効率的な実装とは言えません。
prevItemsLenはこのコンポーネントの描画に必要のないものですが、prevItemsLenが更新される度コンポーネントの描画が起こってしまいます。
さて、ようやく本題に入れます。ここでuseRefの出番です。
上述しましたがuseRefは書き換え可能な値を保持するのに使用することができます。
さらにuseRefのドキュメントにはこうあります。
useRef は中身が変更になってもそのことを通知しないということを覚えておいてください。
useStateで起こっていた問題も大丈夫なようです。
それでは実際に書き換えてみましょう。
function List (items, append, remove) {
const prevItemsLen = useRef(items.length);
const scrollAreaRef = useRef(null);
const itemsLen = items.length;useEffect(() => {
if (itemsLen > prevItemsLen.current && scrollAreaRef) {
scrollAreaRef.current.scrollTop
= scrollAreaRef.current.scrollHeight;
}
prevItemsLen.current = itemsLen;
}, [itemsLen]);return (
<>
<ul
ref={scrollAreaRef}
style={{ maxHeight: '500px', overflowY: 'scroll' }}
>
{
items.map((item) => {
return (
<li key={item.id}>
{item.content}
<button onClick={() => remove(item.id)}>
delete
</button>
</li>
)
})
}
<ul/>
<button onClick={append}>add</button>
</>
)
}
このように、コンポーネントの描画に関係のない値を保持するのにuseRefを活用することができました。
useRef vs 変数
さて、上記のサンプルコードをみてこう思った方もいるのではないでしょうか?
これコンポーネントの外で変数持つ形でもよくね?
ちなみに私は思いました。
そしてそれは間違いでした。これではよくありません。
以下コンポーネント外で変数を持つようにして書いたパターンです。
let prevItemsLen = 0;function List (items, append, remove) {
const scrollAreaRef = useRef(null);
const itemsLen = items.length;useEffect(() => {
if (itemsLen > prevItemsLen && scrollAreaRef) {
scrollAreaRef.current.scrollTop
= scrollAreaRef.current.scrollHeight;
}
prevItemsLen = itemsLen;
}, [itemsLen]);return (
<>
<ul
ref={scrollAreaRef}
style={{ maxHeight: '500px', overflowY: 'scroll' }}
>
{
items.map((item) => {
return (
<li key={item.id}>
{item.content}
<button onClick={() => remove(item.id)}>
delete
</button>
</li>
)
})
}
<ul/>
<button onClick={append}>add</button>
</>
)
}
これの何がダメなのでしょうか?
答えはコンポーネントのunmountにあります。
このコンポーネントがunmountされ、再度mountされた場合prevItemsLenの値はどうなるでしょうか? 前回unmountされた時の値が次にmountされた時に引き継がれ、バグが発生してしまいます。
その点useRefならunmountされる時に一緒に解放されるため安心です。
もちろん、上記例でもコンポーネントのunmount時に変数の初期化を行えばいいのですが、その処理を書くくらいなら最初からuseRefを使った方が簡潔にかけると思います。
終わりに
dom参照以外でuseRefを使うパターンのご紹介でした。
皆さんも楽しいuseRefライフを!