カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

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 を使って関数内で計算を行い、混乱を避けることができます。他の開発者は 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 の場合は favorite のデータをフィルタリングします。この方法の欠点は、データの処理ロジックが 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)」であり、状態自体が 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(...)}
  </>
}

クエリが更新されるたびに状態も更新されるようにしたいですが、この方法では2つの問題があります:

  • データが変更されるたびに、状態を更新する必要があるため、不要な更新が1回追加されます
  • 入力値が prop と state の両方に依存しているため、真の唯一の情報源を維持することができません

開発者にとって最も危険なことは、真の唯一の情報源を維持できないことです。開発者にとっては直感的ではなく、デバッグが非常に困難です。

これらの問題に加えて、実際にはこの方法は正しくありません。なぜなら、useState(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 の大きな利点は、コードが意図を表現するのに役立つことです。見た目は多少オーバーヘッドがあるように見えますが(実際、他のフレームワークではメモ化されていません)、パフォーマンスを犠牲にしても可読性を向上させるためには十分に価値があります。

React のベータ版のドキュメントでは、どこでも useMemo を使用するかどうかについての議論がありますが、個人的には参考になると思います。

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください