🌕

【useCallback】React hookが便利すぎる

8 min read

React hookとは..?

React hookはReact16.8から追加された機能で、クラスコンポーネントでしか使用できなかったstateなどのReactの機能を関数コンポーネントで使用できる機能です。
公式ページは以下です。

https://ja.reactjs.org/docs/hooks-intro.html

React hookのAPIについて紹介していこうと思います。
他のReact hookに関するAPIについても解説していますので、そちらもご覧ください。

useCallbackとは...?

useCallback()はメモ化されたコールバックを返し、その関数は依存配列の要素のいずれかが変化した場合にのみ変化します。
useCallback(fn, deps)useMemo(() => fn, deps)と等価のようです。

メモ化とは...?

Reactのメモ化とは、計算結果を保持し、それを再利用する手法のことです。キャッシュの考え方と同じイメージで良いかと思います。メモ化によって都度計算する必要がなくなるため、パフォーマンスの向上に繋がります。

使い方

  • useCallback(コールバック関数, [依存配列]);のように宣言します。
useCallback(callbackFunction, [deps]);

aの値が変わらない限り、useCallbackによってメモ化されたcallbackFunctionを再利用します。aの値が更新された、新たにcallbackFunctionが生成されます。

const callbackFunction = useCallback(
    () => { doSomthing(a)}, [a]
);

実際にコードで書いてみました。例として、以下の3つの場合を試してみました。

  1. useCallbackを使用しない場合
  2. React.memoでメモ化した場合
  3. React.memo+useCallbackを使用した場合

useCallbackを使用しない場合

Counter4.jsx
import React, { useState } from 'react';
import '../style.css';

// タイトルコンポーネント(子)を定義する。
const Title = () => {
    console.log('★Title component');
    return (
        <p>useCallBackの再レンダーを検証</p>
    )
};

// ボタンコンポーネント(子)を定義する。
const Button = (props) => {
    console.log('★Button component', props.name);
    return (
        <button onClick={ props.doClick }>{ props.name }</button>
    )
};

// カウンターコンポーネント(子)を定義する。
const CounterText = (props) => {
    console.log('★Count child component', props.text);
    return (
        <p>{props.text}:{props.state}</p>
    )
};

const Counter4 = () => {
    const [firstCounter, setFirstCounter] = useState(0);
    const [secondCounter, setSecondCounter] = useState(100);

    // +1する関数を定義する。
    const coutUpFirstCounter = () => {
        setFirstCounter(firstCounter + 1);
    };

    // +100する関数を定義する。
    const coutUpSecoundCounter = () => {
        setSecondCounter(secondCounter + 100);
    };

    return (
        <>
            <Title />
            <CounterText text="+1 ボタンによるカウント" state={ firstCounter }/>
            <CounterText text="+100 ボタンによるカウント" state={ secondCounter } />
            <Button name="+1" doClick={ coutUpFirstCounter }/>
            <Button name="+100" doClick={ coutUpSecoundCounter }/>
            <div className='line'></div>
        </>
    )
}

export default Counter4
App.jsx
import React from 'react';
import { Counter4 } from './components/index';

function App() {
  return (
    <div>
      <p>useCallbackのサンプルです</p>
      <Counter4 />
    </div>
  );
}

export default App;

以下のように動作します。

useCallbackを使用していないので、stateとしてfirstCountersecondCounterを用意していますが、どちらかの値が更新されることで、全てのコンポーネント(TitleコンポーネントCounterTextコンポーネントButtonコンポーネント)が再レンダリングされています。
もし、これらのコンポーネントで時間がかかるような処理を行なっていた場合、パフォーマンスに悪影響を及ぼします。

  • React.memoでメモ化した場合
    上記の例のように、再レンダリングの不要なコンポーネントは再レンダリングさせないためにReact.memoでメモ化してみましょう。

以下のように修正してみました。

Counter4.jsx
import React, { useState } from 'react';
import '../style.css';

// タイトルコンポーネント(子)を定義する。
const Title = React.memo(() => {
    console.log('★Title component');
    return (
        <p>useCallBackの再レンダーを検証</p>
    )
});

// ボタンコンポーネント(子)を定義する。
const Button = React.memo((props) => {
    console.log('★Button component', props.name);
    return (
        <button onClick={ props.doClick }>{ props.name }</button>
    )
});

// カウンターコンポーネント(子)を定義する。
const CounterText = React.memo((props) => {
    console.log('★Count child component', props.text);
    return (
        <p>{props.text}:{props.state}</p>
    )
});

const Counter4 = () => {
    const [firstCounter, setFirstCounter] = useState(0);
    const [secondCounter, setSecondCounter] = useState(100);

    // +1する関数を定義する。
    const coutUpFirstCounter = () => {
        setFirstCounter(firstCounter + 1);
    };

    // +100する関数を定義する。
    const coutUpSecoundCounter = () => {
        setSecondCounter(secondCounter + 100);
    };

    return (
        <>
            <Title />
            <CounterText text="+1 ボタンによるカウント" state={ firstCounter }/>
            <CounterText text="+100 ボタンによるカウント" state={ secondCounter } />
            <Button name="+1" doClick={ coutUpFirstCounter }/>
            <Button name="+100" doClick={ coutUpSecoundCounter }/>
            <div className='line'></div>
        </>
    )
}

export default Counter4

以下のように動作します。

TitleコンポーネントCounterTextコンポーネントButtonコンポーネントReact.memo()関数でラップし、メモ化しています。
2回目以降、以下のような挙動になっています。

  • Titleコンポーネントpropsがないため、再レンダリングされていません。
  • CounterTextコンポーネントは各propsに対応するカウンターが更新されたコンポーネントのみ再レンダリングされているため、最適化されています。
  • Buttonコンポーネントは、両方のボタンが再レンダリングされており、最適化されていません。

両方のボタンが再レンダリングされるのはなぜ...?

<Button name="+1" doClick={ coutUpFirstCounter }/><Button name="+100" doClick={ coutUpSecoundCounter }/>のどちらかのボタンがクリックされた際に、親コンポーネントであるCounter4が再レンダリングされています。この再レンダリングされたタイミングでcoutUpFirstCountercoutUpSecoundCounterも再生成されており、再生成された関数をReact.memoが別の関数と認識するためです。

  • React.memo+useCallbackを使用した場合
    上記でButtonコンポーネントは最適化されませんでした。そこで、React.memo+useCallbackを組み合わせて最適化する方法を紹介します。

以下のように修正してみました。

Counter4.jsx
import React, { useCallback, useState } from 'react';
import '../style.css';

// タイトルコンポーネント(子)を定義する。
const Title = React.memo(() => {
    console.log('★Title component');
    return (
        <p>useCallBackの再レンダーを検証</p>
    )
});

// ボタンコンポーネント(子)を定義する。
const Button = React.memo((props) => {
    console.log('★Button component', props.name);
    return (
        <button onClick={ props.doClick }>{ props.name }</button>
    )
});

// カウンターコンポーネント(子)を定義する。
const CounterText = React.memo((props) => {
    console.log('★Count child component', props.text);
    return (
        <p>{props.text}:{props.state}</p>
    )
});

const Counter4 = () => {
    const [firstCounter, setFirstCounter] = useState(0);
    const [secondCounter, setSecondCounter] = useState(100);

    // +1する関数を定義する。
    const coutUpFirstCounter = useCallback(() => {
        setFirstCounter(firstCounter + 1);
    }, [firstCounter]);

    // +100する関数を定義する。
    const coutUpSecoundCounter = useCallback(() => {
        setSecondCounter(secondCounter + 100);
    }, [secondCounter]);

    return (
        <>
            <Title />
            <CounterText text="+1 ボタンによるカウント" state={ firstCounter }/>
            <CounterText text="+100 ボタンによるカウント" state={ secondCounter } />
            <Button name="+1" doClick={ coutUpFirstCounter }/>
            <Button name="+100" doClick={ coutUpSecoundCounter }/>
            <div className='line'></div>
        </>
    )
}

export default Counter4

以下のように動作します。

以下のように、useCallbackにメモ化するコールバック関数と依存配列を渡しています。
coutUpFirstCounterをonClickに持つボタンは、firstCounterが更新された場合のみ再レンダリングされ、coutUpSecoundCounterをonClickに持つボタンは、secondCounterが更新された場合のみ再レンダリングされるようになりました。

const coutUpFirstCounter = useCallback(() => {
    setFirstCounter(firstCounter + 1);
}, [firstCounter]);

const coutUpSecoundCounter = useCallback(() => {
    setSecondCounter(secondCounter + 100);
}, [secondCounter]);

まとめ

今回の記事ではuseCallbackを紹介しました。次回はuseMemoを紹介しようと思います。
githubにサンプルコードを載せていますので、気になった方はご覧ください。

https://github.com/Tomoki-webpro/react-hooks-study

参考記事

https://ja.reactjs.org/docs/hooks-reference.html#usecallback

https://qiita.com/seira/items/8a170cc950241a8fdb23

Discussion

ログインするとコメントできます