😀

useRefを使った最適化

2023/03/31に公開

はじめに

「子コンポーネントにrefを渡す」という手法のパフォーマンス最適化の方法を紹介します。
最後に私が直面した事例を紹介します。

そもそもuseRefってなんだっけ?

まずuseRefの復習をしましょう。
useRefといえばDOMの操作(focusなどが有名)ですが、他にも使い方があります。そこで、公式ドキュメントから「最適化」の文脈で大事だと思うキーワードをいくつか紹介します。

  • useRefとは、レンダリングに不要な値を参照することができるhookである
  • ref.currentを更新しても再レンダリングされない

要するに、レンダリングに必要のない値 を管理するときに、useStateではなくuseRefを使えば、不要な再レンダリングを防ぐことができます。
公式のuseRefの使用例を挙げます。この例では、クリックした回数をアラートで表示することができます。ref.currentの値を更新しても再レンダリングされません。

export default function Counter() {
  let ref = useRef(0);

  function handleClick() {
    ref.current = ref.current + 1;
    alert("You clicked " + ref.current + " times!");
  }

  return <button onClick={handleClick}>Click me!</button>;
}

本題

では、本題に入ります。
私が最適化のためにやりたいことは、子から親のrefを参照することです。
素直に考えると、propsでref.currentを渡すことを考えますが、ref.currentの値が更新されるとpropsも更新されるため、子コンポーネントは再レンダリングされてしまいます。せっかく「更新されても再レンダリングされない」という性質があるのに、それを子コンポーネントから参照しようとすると再レンダリングされてしまうのは望ましくありません。親のrefが更新されても、再レンダリングされずに更新後のrefを参照することはできるのでしょうか?

解決策は、ref.currentの値ではなく、refオブジェクトそのものをpropsとして渡すことです。refオブジェクトの参照は変わらないため、propsの更新は検知されず、子コンポーネントの再レンダリングを防ぎつつ、子から親の更新後のref.currentの値を参照することができます。

以下は、公式の例を拡張して、親コンポーネントのボタンがクリックされた回数をアラートする子コンポーネントの実装例です。
親コンポーネントのボタンをクリックしてcountRef.currentの値を増やしても、子コンポーネントは再レンダリングされません。

import { MutableRefObject, useRef } from "react";

export const Parent = () => {
  const countRef = useRef<number>(0);
  const handleClick = () => {
    countRef.current = countRef.current + 1;
    alert("You clicked " + countRef.current + " times!");
  };
  return (
    <div>
      <button onClick={handleClick}>parent button</button>
      <Child countRef={countRef} />
    </div>
  );
};

type PropsType = {
  //refという命名は避ける必要がある。(詳しくは、useRefドキュメントのTroubleshooting)
  countRef: MutableRefObject<number>;
};
const Child = (props: PropsType) => {
  const { countRef } = props;
  const handleClick = () => {
    alert("parent button is clicked " + countRef.current + "times");
  };
  return <button onClick={handleClick}>child button</button>;
};

本題は以上です

実際に直面した場面

マウスを押しているときに色が塗られるようになっています。
range

フィールド全体に対して今マウスが押されているかをuseRefで管理して、それを先程紹介した方法で小さなマスのコンポーネントに渡しています。そのコンポーネントのmouseOverイベントに「マウスが押されていれば色を変える」関数を登録しています。
これをuseStateで管理してしまうと、クリックして状態が更新されるたびに13×13×4×4=2704個のマスが再レンダリングされてしまいます。

塗り絵のアプリは個人開発しているポーカーのレンジ構築アプリです。githubリポジトリはこちら

Discussion