useCallbackについて知る
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
trip
のCard
コンポーネントは再レンダリングされなくなったのですが、HobbyEdit
コンポーネントはレンダリングされたままです。
これはユーザー名更新時にTopPage
コンポーネントが更新され、そのときにhandleHobbyChange
コンポーネントも再度読み込まれているためです。
これを改善するにはuseCallback
でhandleNameChange
、handleHobbyChange
をラップして依存配列にそれぞれ、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);