はじめに
Reactにはさまざまなフックが用意されています。
その中でも、useMemo、useCallback、useEffectは第1引数に関数、第2引数に依存配列を指定するフックです。
依存配列の値が更新されたときに、関数の再実行が行われます。
業務で使っていてあまり違いが理解できなかったので、一つずつ役割や使い方を確認していきます。
useMemo
概要
useMemoは時間がかかる処理の結果を保存することで、コンポーネントの再レンダリングのたびに再計算されるのを防ぐことができます。これにより、パフォーマンスの低下を抑止できます。
基本形
useMemo(calculateValue, [dependencies]);
引数
calculateValueには、時間のかかる処理を実行する関数を指定します。
この関数は引数を指定することができず、何らかの値を戻り値として返す必要があります。
dependenciesには、関数内で使用しているリアクティブな値をすべて配列で指定します。
リアクティブな値とは、propsや、state、コンポーネントで定義されている変数、関数を指します。
「リアクティブ」とは「(何かに対して)反応する」という意味合いがあり、時間の経過とともに変化する可能性がある値のことをいいます。
戻り値
calculateValueに指定した戻り値が、useMemoの戻り値として設定されます。
実行結果
初回読み込み時には、calculateValueがそのまま呼び出されます。
以降は、dependenciesが変更されたときのみ、calculateValueが呼び出されます。
つまり、再レンダリングされても値の変更がなければ以前の結果をそのまま使用します。
使用例
実際の処理がわかるようなデモを用意しました。
親コンポーネントから渡されたリストを元に、子コンポーネントでリストの中身を合計して出力する処理です。
リストの中身を合計するたび、「Calculating...」というログをコンソールに出力します。
「Clicked x times」は子コンポーネントの再レンダリングを発生させます。
「Toggle」は親コンポーネントの再レンダリングを発生させます。
「Add random number to list」は親コンポーネントから子コンポーネントに渡しているリストを変化させます。
このデモを見ると分かるとおり、コンポーネントの再レンダリングでは計算処理が動いていません。
「Add random number to list」でリストを変更した場合のみ、再計算されているのがわかります。
実際のソースコードは以下です。
親コンポーネント
import { useState } from "react";
import ExpensiveCalculation from "./ExpensiveCalculation";
const ParentComponent = () => {
const [list, setList] = useState([1, 2, 3, 4, 5]);
const [toggle, setToggle] = useState(false);
const addToList = () => {
setList([...list, Math.floor(Math.random() * 100)]);
};
return (
<div>
<ExpensiveCalculation list={list} />
<button onClick={addToList}>Add random number to list</button>
<button onClick={() => setToggle(!toggle)}>
Toggle: {toggle ? "ON" : "OFF"}
</button>
<p>List: {list.join(", ")}</p>
</div>
);
};
export default ParentComponent;
子コンポーネント
/* ExpensiveCalculation.jsx */
import { useMemo, useState } from "react";
const ExpensiveCalculation = ({ list }) => {
const [count, setCount] = useState(0);
const expensiveResult = useMemo(() => {
console.log("Calculating...");
return list.reduce((acc, item) => acc + item, 0);
}, [list]);
return (
<div>
<p>Sum: {expensiveResult}</p>
<button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
</div>
);
};
export default ExpensiveCalculation;
子コンポーネントのuseMemoでは、第1引数にlistの合計処理を、第2引数にpropsのlistを指定しています。
これにより、処理結果を保存し、listが変更されたときのみ再計算することが可能になりました。
useCallback
概要
useCallbackは関数そのものを保存しておくことができます。useMemoと違い、関数の実行はされません。
通常、コンポーネント内に定義された関数は再レンダリングのたびに新しく定義されますが、useCallbackを使うことで、再レンダリング後も同じ関数を保持しておくことができます。
useCallbackは単体で使うこともありますが、memoという別のReactのフックと組み合わせて使うことが多いようです。詳細は使用例で確認します。
基本形
useCallback(fn,[dependencies])
引数
fnには保存したい関数を指定します。
この関数は任意の引数を指定することができます。指定しなくてもよいです。
また、戻り値の設定も任意です。
dependenciesには、useMemoと同様、fn内で使用しているリアクティブな値をすべて指定します。
ここでのポイントは、fnに指定した引数はリアクティブな値ではないので、dependenciesに指定しなくてよいということです。
以下のコード例で確認します。
import { useCallback } from "react";
const Component = (props) => {
let num = 0;
const cb = useCallback(
(val) => {
console.log(props);
console.log(num);
console.log(val);
},
[props, num]
);
cb("test");
};
props、numはリアクティブな値なので、dependenciesに指定する必要があります。
valも変化する可能性があるのでリアクティブと考えられそうですが、そうではありません。
valの値は「test」が引数として指定される場合、常に「test」になります。
しかし、propsやnumは外部の要素によって変更される可能性があるため、関数実行時に常に同じ値である保証がありません。
ややこしいですが、大事なポイントなのでこの違いを抑えておきましょう。
戻り値
fnそのものがuseCallbackの戻り値として設定されます。関数の実行はされません。
関数そのものが返ってくるので、変数に格納することで任意の箇所で関数の実行ができます。
(上記のコード例だと「cb」として扱い、任意のタイミングで実行している。)
使用例
関数を保存することによって何ができるようになるのかイメージできるよう、以下のデモで確認していきます。
Count、Dependencyはともに親コンポーネントで定義したstateです。
Increment Count、Increment Dependencyは、それぞれstateを1ずつ増やします。
Click meは子コンポーネントで定義したボタンで、親コンポーネントから渡された関数を実行します。
親コンポーネントが読み込まれると「Parent rendered」がコンソールに出力されます。
子コンポーネントが読み込まれると「Child rendered」がコンソールに出力されます。
Click meを押下すると、現在のDependencyの値がコンソールに出力されます。
子コンポーネントは、memoというReactフックでラップされています。
これは、コンポーネントに渡されるpropsをチェックし、propsが変化していなければ、コンポーネントの再レンダリングを行わないというものです。
useCallbackと組み合わせることで、子コンポーネントの不必要な再レンダリングを防ぐことができます。
デモを見るとわかるように、Increment Countをクリックするとstateが更新されるので、親コンポーネントが再レンダリングされます。
親コンポーネントが再レンダリングされると、子コンポーネントも再レンダリングされるはずですが、そのようにはなっていません。
一方、Increment Dependencyをクリックすると、親コンポーネントも子コンポーネントも再レンダリングされていることがわかります。
これは、memoとuseCallbackの組み合わせによるものです。
実際のコードを確認します。
親コンポーネント
/* ParentComponent.jsx */
import { useCallback, useState } from "react";
import ChildComponent from "./ChildComponent";
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [dependencyValue, setDependencyValue] = useState(0);
const handleClick = useCallback(() => {
console.log(`Button clicked. Value: ${dependencyValue}`);
}, [dependencyValue]);
console.log("Parent rendered");
return (
<div>
<p>Count: {count}</p>
<p>Dependency: {dependencyValue}</p>
<button onClick={() => setCount(count + 1)}>Increment Count</button>
<button onClick={() => setDependencyValue(dependencyValue + 1)}>
Increment Dependency
</button>
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
子コンポーネント
/* ChildComponent.jsx */
import { memo } from "react";
const ChildComponent = memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click me</button>;
});
export default ChildComponent;
子コンポーネントはmemoでラップされています。
これにより、onClickが変化したときのみ再レンダリングされます。
親コンポーネントでは、useCallbackを使って、子コンポーネントにわたす関数handleClickを取得しています。依存配列にはdependencyValueを指定しています。
これにより、handleClickはdependencyValueが変化したときのみ新しくなります。
つまり、dependencyValueが変化しない限り、子コンポーネントが再描画されない、ということになります。
useCallbackとmemoを組み合わせることで、子コンポーネントが無駄に再レンダリングされるのを防ぐことができるようになります。
useEffect
概要
useEffectは外部システムとコンポーネントを同期させるために使用します。
外部システムとはReactの制御の外にあるもので、サーバーへの接続や、Windowオブジェクトのイベントのリッスン、サードパーティライブラリへの接続などを指します。
基本形
useEffect(setup, [dependencies])
引数
setupは外部システムへの接続を行う処理を記載した関数です。
この関数は引数を指定しません。戻り値として関数を指定することができます。
戻り値に指定した関数はクリーンアップ関数と呼ばれ、コンポーネントが再レンダリング、およびアンマウントされるときに実行されます。
外部システムとの接続を切る処理を書くことがほとんどです。
dependenciesはsetupで使用しているリアクティブな値をすべて指定します。
dependenciesに指定した値が更新されるたび、クリーンアップ関数があればまずそれを実行し、次にsetupが実行されます。
戻り値
useEffectの戻り値はありません。
使用例
useEffectの役割をわかりやすくするため、以下のデモで確認していきます。
Reactのコードの管理外、つまり外部システムにあるウィンドウサイズを取得するものです。
Countと現在のウィンドウ幅はstateで管理している値です。
Click Meをクリックすることで、stateを1ずつ増やします。
現在のウィンドウ幅は、ウィンドウサイズをリサイズする事にその値を取得し、反映します。
コンポーネントが読み込まれると「component rendered」がコンソールに出力されます。
useEffectが実行されると「setup called」がコンソールに出力されます。
クリーンアップ関数が実行されると「cleanup called」がコンソールに出力されます。
ウィンドウサイズをリサイズすると、ウィンドウ幅が即座に反映され、コンポーネント再レンダリング、クリーンアップ関数の実行、useEffectの実行の順で動いているのがわかります。
また、Click meをクリックしてもuseEffectは実行されず、コンポーネントの再レンダリングのみが発生していることがわかります。
useEffectを使用することで、Reactのコンポーネントの外にあるシステムと、Reactのコンポーネントを同期させることができます。
実際のコードは以下です。
/* WindowResizeTracker.jsx */
import { useState, useEffect } from "react";
const WindowResizeTracker = () => {
const [count, setCount] = useState(0);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
console.log("component rendered");
useEffect(() => {
console.log("setup called");
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener("resize", handleResize);
return () => {
console.log("cleanup called");
window.removeEventListener("resize", handleResize);
};
}, [windowWidth]);
return (
<div>
<h3>ウィンドウサイズトラッカー</h3>
<p>Count: {count}</p>
<p>現在のウィンドウ幅: {windowWidth}px</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};
export default WindowResizeTracker;
まとめ
useMemo、useCallback、useEffectのそれぞれの役割について確認してきました。
引数に設定する値はどれも似通っていますが、まったく違うものであることがわかりました。
正しいフックの使い方を理解することで、より効果的にReactを使っていきたいと思いました。