useMemo のいくつかの使用シーン
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 が変更され、filtered が true の場合は 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
もう一つのユースケースは「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(...)}
</>
}
query が更新されるたびに、コンポーネントの状態も合わせて更新したいとする。しかし、このアプローチには2つの問題がある:
- data が変化するたびに状態をもう一度更新する必要があるため、不要な再レンダリングが1回増える
- 入力値が prop と state の両方に依存するため、single source of truth(信頼できる唯一の情報源)が維持できなくなる
特に開発において最も危険なのは single source of truth が維持できないことであり、開発者にとって直感的でなく、デバッグも極めて困難になる。
先ほど挙げた2つの問題に加えて、実はこの書き方は正しくない。その理由は、useState(value) の引数は初期値として定義されているため、value に変化があっても state は更新されないからだ。これにより、後から data や query が更新されても、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(...)}
</>
}
ある state を宣言し、その state が特定の 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 の大きな利点は、コードの意図を表現しやすくなることだ。多少のオーバーヘッドがあるように見えるかもしれないが(実際その通りだ(笑)、他のフレームワークはそもそも memo 化なんて気にしていない)、多少のパフォーマンスと引き換えにコードの可読性を得られるなら、十分に価値がある。
React のベータ版ドキュメントには、いたるところで useMemo を使うべきかどうかについての議論が載っており、個人的にとても参考になると思う。
関連記事
- Three.js で僕の部屋を表現する 僕が React Three Fiber を使って実際の自分の部屋をブラウザ上に再現し、実世界のオブジェクトを目録に見立て、空間の記憶を通してここ数年の生活と仕事について語った話。
- フロントエンドで画像を扱う際に注意すべきこと Jake Archibaldの記事を起点に、現代のレスポンシブ画像の書き方を整理する。なぜwidth/heightを付ける必要があるのか、CSSのaspect-ratioはいつ使うべきか、AVIFとWebPの選び方、そしてpicture/source/srcsetを使ったモバイル向け画像の切り替えについて。
- CSS field-sizing — たった1行のCSSでフォーム要素を自動リサイズする かつてtextareaの自動高さ調整は、JavaScriptでscrollHeightを監視するしかなかった。しかしCSSのfield-sizing: contentなら、わずか1行で代替でき、textarea、input、selectに対応している。本記事では従来のやり方のペインポイントと、field-sizingの使い方をまとめる。
- リンクの下線をもっと見栄え良くする:text-underline-offset デフォルトでは下線と文字が近すぎて、このスタイルを好まないデザイナーもいるし、僕自身もあまり綺麗ではないと感じていた。