【React.js】React hookを理解する! ~ useCallbak編 ~

目次

React hookとは..?

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

公式ページは以下です。

この記事ではReact hookのAPIの1つであるuseCallbackについて紹介していこうと思います。

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

useCallbackとは…?

useCallbak()はメモ化されたコールバックを返し、その関数は依存配列の要素のいずれかが変化した場合にのみ変化します。

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.memouseCallbackを使用した場合

useCallbakを使用しない場合

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
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でメモ化してみましょう。

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

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()関数でラップし、メモ化しています。

TitleコンポーネントConterTextコンポーネントButtonコンポーネントReact.memo()関数でラップしてメモ化しています。

2回目以降、以下のような挙動になっています。

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

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

<Button name=”+1″ doClick={ coutUpFirstCounter }/>Button name=”+100″ doClick={ coutUpSecoundCounter }/>のどちらかのボタンがクリックされた際に、親コンポーネントであるCounter4が再レンダリングされています。

この再レンダリングされたタイミングでcoutUpFirstCountercoutUpSecoundCounterも再生成されており、再生成された関数をReact.memoが別の関数と認識するためです。

React.memo+useCallbackを使用した場合


上記でButtonコンポーネントは最適化されませんでした。

そこで、React.memouseCallbackを組み合わせて最適化する方法を紹介します。

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

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にメモ化するコールバック関数と依存配列を渡しています。

coutUpFirstCounteronClickに持つボタンは、firstCounterが更新された場合のみ再レンダリングされ、coutUpSecoundCounteronClickに持つボタンは、secondCounterが更新された場合のみ再レンダリングされるようになりました。

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

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

まとめ

今回の記事ではuseCallbackを紹介しました。

次回はuseMemoを紹介しようと思います。

参考記事

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次