【React】 memoとuseCallbackで無駄な再レンダを防ぐ

useCallbackは、レンダリング時にコンポーネントの関数が呼び出されるときに無駄な再レンダリングを防ぐものです。

レンダリングの挙動の復習

Reactでは、コンポーネントがレンダリングされる際、そのコンポーネントの関数全体が毎回実行されます。

// xが変わる度に毎回Counterが呼ばれる
export default function Counter() {
    const [x, setX] = useState(10);
  
    const countUp = () => {
        setX(x + 1);
    };

    console.log("called");

    return (
        <div>
            <p>{x}</p>
            <button onClick={countUp}>カウントアップ</button>
        </div>
    );
}

そして、コンポーネントが更新されると、子のコンポーネントの関数も呼ばれます。例えば、

App.jsx
import Counter from './components/Counter';

export default function App() {
    const [s, setS] = useState("");
    const setString = (event) => {
        setS(event.target.value);
    }

    return (
        <div>
            <input type="text" value={s} onChange={setString} />
            <Counter id={1} />
            <Counter id={2} />
            <Counter id={3} />
        </div>
    );
}
components/Counter.jsx
/**
 * @typedef {{
 *  id: number
 * }} Props
 */

/**
 * @param {Props} props 
 * @returns 
 */
export default function Counter(props) {
    // 上の例と同じ

    console.log(`called: ${props.id}`);

    return (
        // ...
    );
}

というコードがあった場合、App関数内にあるinputの入力状態を更新すると、子コンポーネントである全てのCounter関数も呼ばれます。

propsで値を渡した場合

上の例では自分自身で値も管理していましたが、例えば親が値を持っていて、子がpropsで受け取った関数を呼ぶことで値を更新したとします。

App.jsx
export default function App() {
    const [x, setX] = useState(10);
    const [y, setY] = useState(10);

    const countUpX = () => {
        setX(x + 1);
    };

    const countUpY = () => {
        setY(y + 1);
    };

    return (
        <div>
            <p>x: {x}</p>
            <p>y: {y}</p>
            <Counter id={1} countUp={countUpX} />
            <Counter id={2} countUp={countUpY} />
        </div>
    );
}
components/Counter.jsx
export default function Counter(props) {
    console.log(`called: ${props.id}`);

    return (
        <div>
            <button onClick={props.countUp}>+1</button>
        </div>
    );
};

実際に動作させてみると、xを更新するボタン1とyを更新するボタン2のどちらを押しても、2つのボタンのコンポーネントが更新されることがわかります。

xしか更新していないのに、yしか影響しないボタンまで更新されるのは明らかに無駄です。そこで、useCallbackmemoを使用します。

React.memo

memoは、コンポーネントに付与するものです。再レンダされそうになったときに状態をチェックし、再レンダ不要であれば再レンダされないようにするというものです。Propsの関係で使用することが多いと思います。

無駄に再呼び出しされる例

まずは無駄にコンポーネントが再呼び出しされる例を見てみましょう。

export default function App() {
    const [x, setX] = useState(10);
    const [y, setY] = useState(10);

    const inputX = (event) => {
        setX(event.target.value);
    }
    const inputY = (event) => {
        setY(event.target.value);
    }

    return (
        <div>
            <label>
                inputX: <input type="text" value={x} onChange={inputX} />
            </label>
            <label>
                inputY: <input type="text" value={y} onChange={inputY} />
            </label>

            <ViewValue id="x" value={x} />
            <ViewValue id="y" value={y} />

        </div>
    );
}
components/ViewValue.jsx
export default function ViewValue(props) {
    console.log(`called: ${props.id}`);

    return (
        <div>
            <p>{props.id}: {props.value}</p>
        </div>
    );
};

実際に動かしてみると、xだけを更新したにもかかわらず、値に変化がないyの方まで再度コンポーネントが呼び出されていることがわかります。(下の例でconsoleを開いてみてください。)

memoを使う

このような場合に子コンポーネント側でmemoを使用することで、再呼び出しを防ぐことができます。

import { memo } from 'react';

export default memo((props) => {
    console.log(`called: ${props.id}`);

    return (
        <div>
            <p>{props.id}: {props.value}</p>
        </div>
    );
});

実際に例を動かすと、xを変更したときはxの表示を行うコンポーネントだけが更新されていることがわかります。

useCallback

上の場合は、useStateした変数に関するものでした。もちろんconstな変数であれば値が不変なため特に考える必要はありません。しかし、関数をPropで渡した場合は異なります。

なぜconstなのに値が変わった扱いになるのか

constな値は、プリミティブ型であればその値そのものが入ります。しかし、オブジェクト型の場合、値の参照が入ります。中の値が同じでも代入する度にオブジェクトが新規作成され、異なる参照が代入されてしまいます。

Javascriptの比較演算子では、オブジェクト型の場合は中の値で比較されるわけではないということです。

例えば、

const x = 10;
const y = 10;
console.log(x == y);

trueが出力されますが、

const x = [1, 2, 3];
const y = [1, 2, 3];
console.log(x == y);

では、[1, 2, 3]はオブジェクト型であるため参照が異なり、falseが出力されます。これは関数でも同じことがいえます。

const x = () => {};
const y = () => {};
console.log(x == y); // false

しかし、使用しているオブジェクトが同じものを指している場合は、同じものとして認識されます。

const x = () => {};
const y = x;
console.log(x == y); // true

厳密にはObject.isが使用されています。Object.is仕様に関してはMDNを参照してください。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object

useCallbackを使う

useCallbackを使用することで、関数をメモ化します。これを使用することで、同じ関数を使い回すことができるということです。上の例で言うy = xができるということです。

useCallbackの第1引数には動作させたい関数を、第2引数には関数を作り直す条件を入れます。

関数を更新する理由は、クロージャの仕様によるものです。レンダリングの度に関数が呼び出されるため、useStateした値などを更新すると、関数も作り直さないと古い変数を参照してしまうことになります。

export default function App() {
    const [x, setX] = useState(10);
    const [y, setY] = useState(10);

    // xの値が変わらない限り使い回す
    const countUpX = useCallback(() => {
        setX(x + 1);
    }, [x]);

    // yの値が変わらない限り使い回す
    const countUpY = useCallback(() => {
        setY(y + 1);
    }, [y]);

    return (
        <div>
            <p>x: {x}</p>
            <p>y: {y}</p>

            <Counter id="x" countUp={countUpX} hoge={hoge} />
            <Counter id="y" countUp={countUpY} hoge={hoge} />
        </div>
    );
}

呼び出す際は、いつも通りcountUpX()のようにして呼び出します。

上の例では、関数をuseCallbackしたことで、1つの関数を使い回せるようになりました。

constな配列などでも同じことが言える?

はい、言えます。次の例を動かすと、useCallbackしなかった関数と同じような挙動を取ります。

export default function App() {
    // ...
    
    const hoge = [1, 2, 3];

    return (
        <div>
            <p>x: {x}</p>
            <p>y: {y}</p>

            {/* 追加でhogeを渡す */}
            <Counter id="x" countUp={countUpX} hoge={hoge} />
            <Counter id="y" countUp={countUpY} hoge={hoge} />
        </div>
    );
}

そうすると、どうやっても両方のCounterが再呼び出しされることがわかります。この場合、

const [hoge, setHoge] = useState([1, 2, 3]);

// 配列を変更する際は、setHoge([1, 2, 3, 4]); のように配列を作り直す必要あり

のようにしてオブジェクトを使い回すことで解決します。

まとめ

memouseCallbackは、どちらも負荷を軽減するためのものです。基本的にはコンポーネントにmemoを付与し、関数をコンポーネント内で作る際はuseCallbackを使用することをおすすめします。

react usecallback thumb

役に立ったらシェアしよう!