useMemoの使い方

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

useMemo のいくつかの使用シーン

フロントエンドアプリケーションでは、画面に表示される値が他の値を基に計算されるケースがよくあります。あるいは、計算された結果が画面に表示されるという形です。例えば:

  • 時間:秒を xx:xx 分秒の形式に変換して表示
  • フィルターオプションを選択した場合、データは処理されてから表示される

computed prop として使用

次のように書くことができます:

const FormatTime = (ts) => {
  const formattedTime = `${Math.floor(ts/60)}:${ts%60}`
  return <time>{formattedTime}</time>
}

このように書くことには大きな問題はありませんが、「何らかの計算を経てから表示する」といったケースでは、useMemo を使って意図をより明確に表現するのが好きです:

const FormatTime = (ts) => {
  const formattedTime = useMemo(() => {
    return `${Math.floor(ts/60)}:${ts%60}`
  }, [ts])
  
  return <time>{formattedTime}</time>
}

時間フォーマットのこの例では、両者の違いはあまり大きくありません。元の書き方でも十分に簡潔で、計算量も少ないため、useMemo を使うのは過剰最適化のように見えます。しかし、実際のシーンでは、依存する値が増えるにつれて、useMemo を使うことで開発者は関数内で計算を行い、ternary を多用することによる混乱を避けることができます。他の開発者が useMemo を見たときにも「ここでの計算はこれらの変数に依存しており、別の変数を生成する」とすぐに理解できるのです。

ある人は反論するかもしれませんが、それは関数でラップするのと何が違うのでしょうか?例えば:

const Component = (ts) => {
  const computed = calculateMyProp(ts)
  return <div>...</div>
}

私の見解では、この計算が非常に一般的な関数でない限り、計算を関数にラップすることはせいぜいコンポーネントの行数を減らすだけであり、開発者は実装を確認するために関数内部に飛ばなければならず、それならば直接 useMemo を使って実装を書いた方が良いでしょう。

状態に依存する計算

別の使用シーンは、データが他のフィルターに依存している場合です。例えば:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
    {data.filter(d => filtered ? d.favorite : true).map(...)}
  </div>
}

ユーザーがボタンを押すと、filtered が変わります。filteredtrue の場合は、お気に入りのデータがフィルタリングされます。この書き方には一つの欠点があります。データの処理ロジックが JSX 内に書かれているため、ロジックが複雑になると保守が難しくなります。そこで、単一の変数として分割できます:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
+ const filteredData = data.filter(d => filtered ? d.favorite : true)
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
+   {filterData.map(...)}
  </div>
}

こうすることで、JSX 内の表現がより簡潔になりますが、filteredData の変数宣言の実装は直感的ではなく見えます。このケースでは条件が比較的単純ですが、先ほども述べたように、依存が増えると書くのが難しくなります。

このとき、計算を useMemo に移します:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  const [sorted, setSorted] = useState(false)
  const filteredData = useMemo(() => {
    if (filtered) {
      return data.filter(d => d.favorite)      
    }

    return data
  }, [data.length, filtered])
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
  	{filterData.map(...)}
  </div>
}

もし新たに sorted というオプションを追加した場合、useMemo の中で対応する処理と依存を追加すれば良いのです:

const MyComponent = ({ data }) => {
  const [filtered, setFiltered] = useState(false)
  const [sorted, setSorted] = useState(false)
  const filteredData = useMemo(() => {
    const origin = [...data]
    if (filtered) {
      return origin.filter(d => d.favorite)      
    }

    if (sorted) {
      return data.sort()
    }

    return origin
  }, [data.length, filtered, sorted]) // ここでは data の変化は長さが変わったときだけと仮定
  return <div>
    <button onClick={() => setFiltered(s => !s)}>toggle filtered</button>
    {filterData.map(...)}
  </div>
}

この書き方の利点は、依存の宣言が非常に明確になることです。開発者はこのデータがどの変数に依存しているかを一目で理解できます。

derived state

もう一つの使用シーンは「derive state」、つまり状態が props の変化に依存する場合です。上の例を改写します:

const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const [filteredData, setFiltered] = useState(data.filter(d => query ? d.includes(query) : true))
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}
  </>
}

私たちは、query が更新されるたびにコンポーネントの状態も更新されることを望んでいます。しかし、この書き方には二つの問題があります:

  • データが変化するたびに、状態を再度更新する必要があるため、不要な更新が一回増えます。
  • 入力値が props と state の両方に依存しているため、single source of truth が維持できません。

開発者にとって最も危険なことは、single source of truth が維持できないことです。これは直感的でなく、デバッグが非常に難しいのです。

先ほど述べた二つの問題を除いても、この書き方は実際には正しくありません。その理由は、useState(value) の引数が初期値として定義されているため、value が変化しても状態は変わらないからです。これにより、dataquery が更新されても、filteredData は最初のレンダリング時の値のままとなります。

useState の引数は最初のレンダリング時にのみ有効です。正しい(ただし推奨されない)書き方は次の通りです:

/* この書き方は推奨されません */
const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const [filteredData, setFiltered] = useState(data.filter(d => query ? d.includes(query) : true))
+ useEffect(() => { data.filter(d => d.includes(query)) }, [data, query])
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}

  </>
}

状態を宣言し、その状態が特定の props に依存する場合、ほとんどの場合は useMemo に書き換えることができます。この例では、先ほどの例とほぼ同じで、useMemo を使って書き換えることができます:

const MyComponent = ({ data }) => {
  const [query, setQuery] = useState(value)
  const filteredData = useMemo(() => {
    if (query) {
      return data.filter(d => d.includes(query))
    }
    
    return data
  }, [data, query])
	
  return <>
    <input onChange={e => setQuery(e.target.value)} value={query} />
  	{filteredData.map(...)}

  </>
}

まとめ

上記のいくつかの例から、以下の点をまとめることができます:

  • computed props は useMemo を使って他の開発者のコードの認知負担を軽減できます。
    • 計算量が多い場合、毎回のレンダリングで再計算するコストを節約できます。
  • useState(props) を使用する際は特に注意が必要です。
  • derived state はほとんどの場合、useMemo を使って解決できます。

また、私にとって useMemo の大きな利点は、コードの意図を明確にすることです。一見するとオーバーヘッドのように見えますが(実際にそうなんですがXD、他のフレームワークではメモ化は行われていません)、わずかなパフォーマンスの犠牲で可読性を向上させる価値は十分にあります。

React beta のドキュメントには、useMemo をどのように使うべきかについての議論があり、個人的には非常に参考になると思います。

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee