質問やフィードバックがありましたら、フォームからお願いします
本文は台湾華語で、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
が変わります。filtered
が true
の場合は、お気に入りのデータがフィルタリングされます。この書き方には一つの欠点があります。データの処理ロジックが 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 が変化しても状態は変わらないからです。これにより、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(...)}
</>
}
状態を宣言し、その状態が特定の 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