Yanonoblog!

こつこつと

useEffectを使用する上での注意点とuseMemoの概要

はじめに

本記事では「モダンJavaScriptの基本から始める React実践の教科書」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。

詳細は2023年の3月にリリースされた新しいReactの公式ドキュメントを参照してまとめています。

書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。

主な引用元: https://react.dev/learn/you-might-not-need-an-effect

props または state に基づいて状態を更新する

コンポーネントfirstNamelastNameという2つの状態変数を持つとした場合、連結して算出したのがfullNameです。

firstNamelastNameのどちらかが変われば、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;
    // ...
}
  • 既存の props または state から何かを計算できる場合は、それを state に入れない
  • 既存のプロパティや状態から算出できる値は、レンダリング中に計算する
  • コードがシンプルとなり、異なる状態変数が同期できないことによるバグを回避する
  • 負荷の高い計算をキャッシュする

    アンチパターン: 計算結果を状態変数に入れてエフェクトで更新する

    下記の例では関数コンポーネントTodoListの中で引数に渡されたtodosfilterを用いてフィルタリングした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に伝えるのは、依存するtodosfilterが変わらないかぎり内部関数getFilteredTodos()は再実行しません

    React は、最初のレンダリング中の、getFilteredTodos()の戻り値を記憶し、

    次のレンダリング時に依存するtodosまたは がfilterが異なるかどうかがチェックします。

    前回と変わりなければ、useMemoは内部関数は実行せずに、保持した直近の値を返します。

    依存値に変更があったとき、関数を呼び出して新たな値を返して、その戻り値はまた新たにキャッシュされます。

    useMemoに包まれた関数が実行されるのはレンダリング時です。したがって、関数の処理は純粋な演算でなければなりません。はじめてのレンダリングでは関数が必ず実行されますので、その処理は速められないことにご注意ください。

    ラップした関数は[useMemo](https://react.dev/reference/react/useMemo)レンダリング中に実行されるため、これは純粋な計算にのみ機能します。

  • useMemoは、負荷の高い演算結果をキャッシュに持つために使用される。
  • useMemoは関数の結果を記憶し、依存する値が変わらない限り再計算しない。
  • useMemoは関数の実行を最初のレンダリング時に行い、その結果を保持する。
  • 参考

    続く…

    コメント

    本記事の内容は以上になります!

    書籍の続きのアウトプットも随時更新したいと思います。


    プログラミングスクールのご紹介 (卒業生より)

    お世話になったプログラミングスクールであるRUNTEQです♪

    https://runteq.jp/r/ohtFwbjW

    こちらのリンクを経由すると1万円引きになります。

    RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。

    もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。

    https://twitter.com/outputky