ReactのuseMemoフックを簡単な例で理解する

React Hooksを使うと、副作用や状態管理を備えた、無駄のない軽量で再利用可能な機能的コンポーネントを書くことができます。

この記事では、フックの概念や、より一般的な useState/useEffect フック、依存性配列の概念について、すでに理解していることを前提としています。

この記事では、あまり知られていないフックの一つである useMemo についてお話したいと思います。

 

デモアプリ

 

このアイデアを実証するために、最もシンプルなアプリは何かを考えてみました。

 

ひとつだけ確かなことは、カウンターと、そのカウンターを増加させるボタンがあることです。

import React, { useState } from 'react';
import "./App.css";const App = () => {
const [count, setCount] = useState(0);
console.log('App rendered with count', count) return (
<div classname="App">
<h1>Hello World</h1>
Counter: {count}

<button onclick="{()"> setCount(count + 1)}>
Increment Count
</button>

</div>
);
}export default App;

試してみてください。インクリメントボタンをクリックするたびに、Appコンポーネントが再レンダリングされ、以下のようなコンソールログが表示されます。

App rendered with count 1
App rendered with count 2
App rendered with count 3
App rendered with count 4
...
// you get the point

とてもシンプルでしょう?でも、もうちょっと必要です。

モックAPIからのデータ取得と、取得したデータに基づいた重い計算を追加したいのです。

新しいコードはハイライトされています。

 

import React, { useEffect, useState } from 'react';
import "./App.css";const fetchData = () => {
// Imagine here an API call which returns a random number return Math.random();
}const runHeavyCalc = data => {
if (!data) return; console.log('Computing heavy func with data', data);
// Math.floor is not heavy, use your imagination again
return Math.floor(data * 100);
}const App = () => {
const [count, setCount] = useState(0);
const [data, setData] = useState();
console.log('App rendered with count', count); useEffect(() => {
const data = fetchData();
setData(data);
}, []) const result = runHeavyCalc(data); return (
<div classname="App">
<h1>Hello World</h1>
Counter: {count}

Result is {result}

<button onclick="{()"> setCount(count + 1)}>
Increment Count
</button>

</div>
);
}export default App;

OKです。ここで追加したのは、APIからデータを取得するためのuseEffectフックです。取得はもともと非同期なので、結果が戻ってきたときにstateに保存します(簡潔にするため、非同期関数を使わずに書いています)。

そして、その上でrunHeavyCalc関数を実行して、結果を画面に表示しています。

 

それでは、ボタンを何度かクリックして、コンソールを見てみましょう。

 

App rendered with count 0
App rendered with count 0
Computing heavy func with data 0.20850214111434084
App rendered with count 1
Computing heavy func with data 0.20850214111434084
App rendered with count 2
Computing heavy func with data 0.20850214111434084
App rendered with count 3
Computing heavy func with data 0.20850214111434084

ここで何が起こったのでしょうか?

最初のメッセージが2回表示されているのは、アプリが最初にカウンタ0でレンダリングされ、APIの取得によって状態が変更されたときに再びレンダリングされたためです。
カウンタの増加ボタンをクリックするたびに App コンポーネントが再レンダリングされ、新しいカウンタが印刷されました。
レンダリングのたびに重い計算が行われ、計算前の乱数が印刷されます。

問題がわかりましたか?

そうでない場合は、コードに戻って次の質問に答えてください。
重い計算は何に依存していますか?

答え:データ変数の値に依存しています。これは、(モック)APIコールから得られる数値です。そして、そのAPIコールは一度だけ行われます。

 

useEffect(() => {
const data = fetchData();
setData(data);
}, [])

 

覚えておいてください。useEffectで依存関係の配列を空にすることで、このフックは昔のComponentDidMountのように最初のレンダリング時にのみ実行されます。

では、なぜ重い計算を何度も実行するのか、という難しい疑問が出てきます。

useMemoで解決

重い計算を呼び出している行をもう一度見てみましょう。

const result = runHeavyCalc(data);

その答えは、Reactが計算を再実行すべきでないことを知らないからです。Reactはそれが計算であることさえ知らず、レンダリングのたびに実行されるただのコードラインなのです。

この計算を一度しか実行しないようにするために、useMemoフックをインポートして、この行をリファクタリングします。


import React, { useEffect, useMemo, useState } from 'react';...const result = useMemo(() => runHeavyCalc(data), [data]);

 

useEffectの構文と同様に、渡されたコールバックは、依存関係のある配列の中で値が変化したとき(ここではデータの値が変化したとき)にのみ実行されます。

新しいコードでは、ボタンを何度かクリックすると、次のようなコンソールログが表示されます。

 


App rendered with count 0 App rendered with count 0 Computing heavy func with data 0.8266147650312481 App rendered with count 1 App rendered with count 2 App rendered with count 3

 

成功しました。これで、APIからデータが返ってきたときに、一度だけ計算が実行されるようになりました。

おわりに

以上、useMemo の威力を簡単にご紹介しました。
useMemo を使用するにはパフォーマンス上のコストがかかるため、プリミティブな代入や短い配列の繰り返しをメモするのには適していません。

プリミティブな割り当てや短い配列の反復処理のメモには適していません。重いデータの解析や大量の反復処理を含む計算の最適化として使用することをお勧めします。試行錯誤が大切ですが、特定のユースケースでは、メモライズの有無でベンチマークを取ってパフォーマンスを比較することもできます。

ご質問・ご意見はありませんか?以下にご記入ください。
お読みいただきありがとうございました。

藤沢瞭介(Ryosuke Hujisawa)
  • りょすけと申します。18歳からプログラミングをはじめ、今はフロントエンドでReactを書いたり、AIの勉強を頑張っています。off.tokyoでは、ハイテクやガジェット、それからプログラミングに関する情報まで、エンジニアに役立つ情報を日々発信しています!

未整理記事

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です