useEffectを使用する上での注意点とuseMemoの概要
はじめに
本記事では「モダンJavaScriptの基本から始める React実践の教科書」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
詳細は2023年の3月にリリースされた新しいReactの公式ドキュメントを参照してまとめています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
主な引用元: https://react.dev/learn/you-might-not-need-an-effect
props または state に基づいて状態を更新する
コンポーネントがfirstName
とlastName
という2つの状態変数を持つとした場合、連結して算出したのがfullName
です。
firstName
とlastName
のどちらかが変われば、fullName
は更新されなければなりません。
アンチパターン: 既存のプロパティや状態から算出できる値に別の状態を加える
fullName
を状態変数に加え、値はエフェクトで更新する例です。
このようにすると状態を不必要に複雑化させるだけで、非効率です。状態変数のfullName
がエフェクトで更新されることにより、再レンダリングが生じてしまいます。
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // 🔴 NG: 状態が冗長で不要なエフェクトが発生する const [fullName, setFullName] = useState(''); useEffect(() => { setFullName(firstName + ' ' + lastName); }, [firstName, lastName]); // ... }
改善例: 既存のプロパティや状態から算出できる値はレンダリング時に演算する
冗長な状態変数fullName
とその更新エフェクトを除き、値をレンダリング時に演算するように改修した例です。
function Form() { const [firstName, setFirstName] = useState('Taylor'); const [lastName, setLastName] = useState('Swift'); // ✅ OK: レンダリング時に演算する const fullName = firstName + ' ' + lastName; // ... }
負荷の高い計算をキャッシュする
アンチパターン: 計算結果を状態変数に入れてエフェクトで更新する
下記の例では関数コンポーネントTodoList
の中で引数に渡されたtodos
とfilter
を用いてフィルタリングしたvisibleTodos
を算出しています。
function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // 🔴 NG: 冗長な状態と無駄なエフェクト const [visibleTodos, setVisibleTodos] = useState([]); useEffect(() => { setVisibleTodos(getFilteredTodos(todos, filter)); }, [todos, filter]); }
改善例: 既存のプロパティや状態から算出できる値はレンダリング時に演算する
冗長な状態変数と演算をエフェクトから除き、値をレンダリング時に演算するように改修した例です。
function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); // ✅ OK: 関数の負荷が高くない場合 const visibleTodos = getFilteredTodos(todos, filter); }
改善例2: 負荷の高い演算を行う場合はuseMemoを使用する
大抵の場合、先程のコードで問題ありません。
関数getFilteredTodos()
の処理や、todo
のデータ量が重かったりする場合もは、関数の処理に関係のない状態(たとえばnewTodo
)が変わったときは再計算させないのが好ましいです。
負荷の高い演算はラップして結果をキャッシュに持つ手法のことを「メモ化」と呼びます。
そのために用いるフックが[useMemo](https://ja.reactjs.org/docs/hooks-reference.html#usememo)
です。
import { useMemo, useState } from 'react'; function TodoList({ todos, filter }) { const [newTodo, setNewTodo] = useState(''); const visibleTodos = useMemo(() => { // ✅ 依存するtodosかfilterに変更がないかぎり再計算しない return getFilteredTodos(todos, filter); }, [todos, filter]); }
useMemo
の引数に渡した関数本体は、1行でも記述可能です。
function TodoList({ todos, filter }) { const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]); }
useMemo
フックがReactに伝えるのは、依存するtodos
かfilter
が変わらないかぎり内部関数getFilteredTodos()
は再実行しません。
React は、最初のレンダリング中の、getFilteredTodos()
の戻り値を記憶し、
次のレンダリング時に依存するtodos
または がfilter
が異なるかどうかがチェックします。
前回と変わりなければ、useMemo
は内部関数は実行せずに、保持した直近の値を返します。
依存値に変更があったとき、関数を呼び出して新たな値を返して、その戻り値はまた新たにキャッシュされます。
useMemo
に包まれた関数が実行されるのはレンダリング時です。したがって、関数の処理は純粋な演算でなければなりません。はじめてのレンダリングでは関数が必ず実行されますので、その処理は速められないことにご注意ください。
ラップした関数は
[useMemo](https://react.dev/reference/react/useMemo)
レンダリング中に実行されるため、これは純粋な計算にのみ機能します。
参考
続く…
コメント
本記事の内容は以上になります!
書籍の続きのアウトプットも随時更新したいと思います。
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。