TK-NOTE

技術的なことや趣味、読んだ本についての感想などを投稿してきます。

useCallbackについて知る

Cover Image for useCallbackについて知る
React

useCallback とは何か?

useCallback はパフォーマンス最適化のためのReactHookです。
特定の処理をメモ化して不要な再レンダリングを抑制します。
第一引数にメモ化する処理を、第二引数に依存配列を指定します。

useCallback(処理, 依存配列)


useCallback を使わない実装

以下のような画面を作成してみました。
トップページにCardコンポーネントを使ってユーザー名情報と趣味の情報を表示しています。
また、その下にユーザー名情報と趣味情報をそれぞれ更新できるようにしています。
そしてCardコンポーネント、HobbyEditコンポーネント、AuthorEditコンポーネントの描画時にconsole.logを仕込んでみました。
src/TopPage.js

import React, { useState } from "react";
import AuthorEdit from "./AuthorEdit";
import HobbyEdit from "./HobbyEdit";
import Card from "./Card";

const TopPage = () => {
  const [name, setName] = useState("yamada");
  const [hobby, setHobby] = useState("trip");
  const handleNameChange = (value) => {
    setName(value);
  };

  const handleHobbyChange = (value) => {
    setHobby(value);
  };

  return (
    <div>
      <Card value={name} />
      <AuthorEdit name={name} handleNameChange={handleNameChange} />

      <Card value={hobby} />
      <HobbyEdit hobby={hobby} handleHobbyChange={handleHobbyChange} />
    </div>
  );
};

export default TopPage;


src/Card.js

import React from "react";

function Card({ value }) {
  console.log("Card rendered", value);
  return (
    <div>
      <p>{value}</p>
    </div>
  );
}

export default Card;


src/AuthorEdit.js

import React from "react";

function AuthorEdit({ handleNameChange, name }) {
  console.log("AuthorEdit rendered")
  return (
    <div>
      <label htmlFor="name">名前: </label>
      <input
        id="name"
        type="text"
        value={name}
        onChange={(e) => handleNameChange(e.target.value)}
      />
    </div>
  );
}

export default AuthorEdit;


src/HoddyEdit.js

import React from "react";

function HobbyEdit({ hobby, handleHobbyChange }) {
  console.log("HobbyEdit rendered")
  return (
    <div>
      <label htmlFor="hobby">趣味: </label>
      <input
        id="hobby"
        type="text"
        value={hobby}
        onChange={(e) => handleHobbyChange(e.target.value)}
      />
    </div>
  );
}

export default HobbyEdit;


ログ出力結果からパフォーマンス改善を行う

上記コードで、初回表示時のログは以下のようになりました。
全てのコンポーネントが描画されていることがわかります。

Card rendered yamada
AuthorEdit rendered
Card rendered trip
HobbyEdit rendered


しかし、ユーザー情報を更新した場合("yamada" -> "tanaka")は以下のようにログ出力されてしまいます。

Card rendered tanaka
AuthorEdit rendered
Card rendered trip
HobbyEdit rendered


ユーザー情報のみを更新しただけなので、本来Cardコンポーネント(Trip)とHobbyEditコンポーネントは再レンダリングされるべきではありません。

これを改善するにはまず、各コンポーネントをメモ化します。
src/Card.js

export default React.memo(Card);

src/HobbyEdit.js

export default React.memo(HobbyEdit);

src/AuthorEdit.js

export default React.memo(AuthorEdit);


その状態で再度ユーザー情報更新時のログ出力結果を見てみます。

Card rendered tanaka
AuthorEdit rendered
HobbyEdit rendered


tripCardコンポーネントは再レンダリングされなくなったのですが、HobbyEditコンポーネントはレンダリングされたままです。
これはユーザー名更新時にTopPageコンポーネントが更新され、そのときにhandleHobbyChangeコンポーネントも再度読み込まれているためです。
これを改善するにはuseCallbackhandleNameChangehandleHobbyChangeをラップして依存配列にそれぞれ、name, hobbyを指定することで必要な時のみ再レンダリングされるようにすれば良いです。
src/TopPage.js

  const handleNameChange = useCallback(
    (value) => {
      setName(value);
    },
    [name]
  );

  const handleHobbyChange = useCallback(
    (value) => {
      setHobby(value);
    },
    [hobby]
  );


この状態でユーザー名を再度更新してみました("yamada" => "yamada2")。

Card rendered yamada2
AuthorEdit rendered


また、趣味情報も更新してみました("trip" => "trip2")。

Card rendered trip2
HobbyEdit rendered


最終的なコードは以下の通りです。
src/TopPage.js

import React, { useCallback, useState } from "react";
import AuthorEdit from "./AuthorEdit";
import HobbyEdit from "./HobbyEdit";
import Card from "./Card";

const TopPage = () => {
  const [name, setName] = useState("yamada");
  const [hobby, setHobby] = useState("trip");
  const handleNameChange = useCallback(
    (value) => {
      setName(value);
    },
    [name]
  );

  const handleHobbyChange = useCallback(
    (value) => {
      setHobby(value);
    },
    [hobby]
  );

  return (
    <div>
      <Card value={name} />
      <AuthorEdit name={name} handleNameChange={handleNameChange} />


      <Card value={hobby} />
      <HobbyEdit hobby={hobby} handleHobbyChange={handleHobbyChange} />
    </div>
  );
};

export default TopPage;

src/Card.js

import React from "react";

function Card({ value }) {
  console.log("Card rendered", value);
  return (
    <div>
      <p>{value}</p>
    </div>
  );
}

export default React.memo(Card);

src/HobbyEdit.js

import React from "react";

function HobbyEdit({ hobby, handleHobbyChange }) {
  console.log("HobbyEdit rendered")
  return (
    <div>
      <label htmlFor="hobby">趣味: </label>
      <input
        id="hobby"
        type="text"
        value={hobby}
        onChange={(e) => handleHobbyChange(e.target.value)}
      />
    </div>
  );
}

export default React.memo(HobbyEdit);

src/AuthorEdit.js

import React from "react";

function AuthorEdit({ handleNameChange, name }) {
  console.log("AuthorEdit rendered")
  return (
    <div>
      <label htmlFor="name">名前: </label>
      <input
        id="name"
        type="text"
        value={name}
        onChange={(e) => handleNameChange(e.target.value)}
      />
    </div>
  );
}

export default React.memo(AuthorEdit);