React HooksのuseCallbackを使おう

レンダリング最適化のためにuseCallbackを使おう

useCallbackでコンポーネントの最適化をしよう

こんにちは。Kennyです。今回はReact.jsのHooksであるuseCallbackを解説していきたいと思います。useCallbackは親子コンポーネントにとってとても重要な技術です。stateやstateを使ったロジックを子コンポーネントにpropsで渡す時ありますよね。ほとんどのReactアプリケーションにおいてある話だと思うので是非マスターしてください。合わせてmemo化と呼ばれる機能についても実例を混ぜて解説していきます。実装可能なバージョンは16.8からとなっております。

前提

開発環境

macOS Catalina

MacBook Pro (15inch, 2019)

npm version 6.14.4

nodebrew use v14.0.0(node)

yarn 1.22.4

React 16.13.1

React-Dom 16.13.1

useCallbackを使う前に

useCallbackの概念を説明するにはやはり実装しながらでなければ掴みづらいと思いますので親子コンポーネントを作成していきましょう。

上記のようなアプリケーションを作成します。行うことは

  • タイトルのコンポーネントであるTitle.jsの作成
  • inputのコンポーネントのInput.jsの作成
  • ボタンのコンポーネントであるButton.jsの作成
  • カウントのコンポーネントであるCount.jsの作成

親コンポーネントであるApp.jsに全てインポートして動作確認を行います。

それぞれuseStateを使ってstateをpropsで渡します。useStateをまだ知らないよという方は詳しく解説している記事がございますのでご参照ください。

useCallbackのテストアプリを作成しよう

まずは子コンポーネントであるTitle.jsの全コードです。

// Title.js
import React from "react";

const Title = () => {
  console.log("Title ");
  return <p>useCallbackのテストを行います</p>;
};

export default Title;

Title.jsコンポーネントではコンソールの出力とタイトルのJSXを返すシンプルな構造です。次にCount.jsです。

// Count.js
const Count = ({ text, countState }) => {
  console.log("Count", text);
  return (
    <div style={{ display: "flex", justifyContent: "center" }}>
      <p
        style={{
          borderBottom: " 1px solid #ccc",
          fontSize: ".8rem",
          width: "150px"
        }}
      >
        {text}: <span style={{ marginLeft: "20px" }}>{countState}</span>
      </p>
    </div>
  );
};

export default Count;

Count.jsはpropsを受け取ります。親から渡ってくるのはtext,countStateです。次にButton.jsです。

// Button.js
const Button = ({ handleClick, value }) => {
  console.log("Button ", value);
  return (
    <button
      type="button"
      onClick={handleClick}
      style={{
        backgroundColor: "lightBlue",
        borderRadius: "5px",
        padding: "2px",
        margin: "5px",
        border: "none"
      }}
    >
      {value}
    </button>
  );
};

export default Button;

Button.jsではvalueとhandlClickをpropsで受け取ります。buttonをクリックすると発火するonClickイベントにhandleClickを渡します。次にInput.jsです。

// Input.js
const Input = (props) => {
  const { handleInput, text } = props;
  console.log("props", props);
  return (
    <>
      <input type="text" onChange={handleInput} value={text} />
      {text}
    </>
  );
};
export default Input;

Input.jsもpropsを受け取ります。こちらはその他の受け取り方が違って見えますが、同じものです。分割代入することでpropsの中から欲しいものだけを選び取ることができる方法で処理系のNode.jsでもよく使います。propsですが、handleInputがonChangeに渡されています。inputのvalueにはtextが渡されています。

これで小コンポーネントの全てになります。全ても子コンポーネントがApp.jsと依存関係にあり、stateを受け取っています。(Title.js以外)。ではApp.jsを作成します。

import React, { useState } from "react";
import "./styles.css";

import Title from "./components/Title";
import Input from "./components/Input";
import Count from "./components/Count";
import Button from "./components/Button";

const App = () => {
  const [oneCount, setOneCount] = useState(0);
  const [twoCount, setTwoCount] = useState(10);
  const [text, setText] = useState("");

  const incrementOne = () => setOneCount(oneCount + 1);

  const multipleCounter = () => setTwoCount(twoCount * 2);

  const onChangeInput = (e) => {
    setText(e.target.value);
  };

  return (
    <div className="App">
      <Title />
      <Input handleInput={onChangeInput} value={text} text={text} />
      <Count text="+1 ボタンの表示" countState={oneCount} />
      <Count text="×2 ボタンの表示" countState={twoCount} />
      <Button handleClick={incrementOne} value={" + 1 ボタン"} />
      <Button handleClick={multipleCounter} value={" × 2 ボタン"} />
    </div>
  );
};

export default App;

App.jsに全ての子コンポーネントがインポートされました。ではコードを見ていきます。

useStateを導入して子コンポーネントに渡す

import React, { useState } from "react";

 const [oneCount, setOneCount] = useState(0);
 const [twoCount, setTwoCount] = useState(10);
 const [text, setText] = useState("");

カウントが1づつ増えるstateがoneCountで初期値が0、二倍になるstateがtwoCountで初期値が10としています。inputのvalueが代入されるのがtextで、そのセッター関数がsetTextです。

props表

子コンポーネントprops
Title.jsなし
Input.jstext, handleInput関数
Button.jsvalue, handleClick関数
Count.jstext, countState(oneCount,twoCountのそれぞれ)

この状態で動作確認していきます。

子コンポーネントではコンソール出力がされるはずですが、inputにデータを入れるたびに、buttonをクリックするたびに全ての子コンポーネントが更新に引っ張られてレンダリングしてしまいます。これこそが問題なのです。例えばTitle.jsとInput.jsだけにして、その他をコメントアウトしても同じです。親コンポーネントが更新され レンダリングされるような挙動があれば子も連動して再レンダリングが走ります。子コンポーネントもstateを受け取っている子コンポーネントも親に依存しているのです。

memoを使って記憶させ、更新がないことをReactに伝えよう

レンダリングした後、更新や変更がないなら、差分が発生せず、記憶しているコンポーネントをそのまま使いまわすことができるようmemoという機能がReactにあります。

全ての子コンポーネントには基本memo化することをお勧めします。特に再レンダリングするとメモリーリークになるような肥大化が懸念されるコンポーネントはやっておいた方がいいと思います。おまじないみたいなものです。

import { memo } from "react";

const Title = memo(() => {
  console.log("Title");
  return <p>useCallbackのテストを行います</p>;
});

export default Title;

上記のようにmemoで全てをラップするだけのシンプルな方法です。これでTitle.jsが再レンダリングされることはありません。

その他の子コンポーネントも同じようにmemo化することができますが、stateを受け取っているものに関しては十分ではありません。やはりレンダリングが走ってしまいます。

ここでuseCallbackの出番となります。

useCallbackを使おう

方法としましては、子コンポーネントにstateをpropsで渡している関数を全てuseCallbackでラップして、依存関係のあるstateをuseCallbackの第二引数に渡します。この形はuseEffectを使用したことがある方なら見覚えがある形ですね。

// useEffect  
useEffect(() => {
    console.log(`${name}が更新されました`)
  }, [name])

useEffectはレンダリングが走った後に必ず発火する機能です。副作用を意図的に制御する必要がありますが、細かいuseEffectの使い方は以前に記事にしていますのでまだ使ったことがないよという方はこちらです。APIで外部からデータを取得したいときや、挙動について記述しています。ReactのHooksには欠かせない機能です。

useCallbackをインポートして、関数をラップしていきます。

useCallbackのインポート

import React, { useState, useCallback } from "react";

React HooksのuseCallbackを使用するのにプラグインやモジュールを使用する必要はありません。使用するにはversionが16.8以上である必要があります。

  const incrementOne = useCallback(() => setOneCount(oneCount + 1), [oneCount]);

  const multipleCounter = useCallback(() => setTwoCount(twoCount * 2), [twoCount]);

  const onChangeInput = useCallback(
    (e) => {
      setText(e.target.value);
    },
    [setText]
  );

子コンポーネントに渡す全ての関数をuseCallbackでラップしました。そして第二引数にはその依存関係のあるstateやセッター関数を渡します。この辺りはuseEffectと近しい方法ですね。

子コンポーネントのmemo化

import { memo } from "react";
// Input.js
const Input = memo((props) => {
  const { handleInput, text } = props;
  console.log("props", text);
  return (
    <>
      <input type="text" onChange={handleInput} value={text} />
      {text}
    </>
  );
});
export default Input;
import { memo } from "react";
//Button.js
const Button = memo(({ handleClick, value }) => {
  console.log("Button ", value);
  return (
    <button
      type="button"
      onClick={handleClick}
      style={{
        backgroundColor: "lightBlue",
        borderRadius: "5px",
        padding: "2px",
        margin: "5px",
        border: "none"
      }}
    >
      {value}
    </button>
  );
});

export default Button;
import { memo } from "react";
// Count.js
const Count = memo(({ text, countState }) => {
  console.log("Count", text);
  return (
    <div style={{ display: "flex", justifyContent: "center" }}>
      <p
        style={{
          borderBottom: " 1px solid #ccc",
          fontSize: ".8rem",
          width: "150px"
        }}
      >
        {text}: <span style={{ marginLeft: "20px" }}>{countState}</span>
      </p>
    </div>
  );
});

export default Count;

まとめ

ここまでいかがだったでしょうか。コンソールで確認するとstateの更新と関係のないコンポーネントからのレンダリングの依存性が解消されたのではないかと思います。この特徴を知らずにメモリーリークが発生しかねない実装方法になるのは初学者の陥りやすいところですのでしっかコンポーネントの最適化、再レンダリングの特徴を掴んで最適化されたReact開発をやっていきましょう。今回は以上です。ありがとうございました。

SNSでもご購読できます。

検索