TK-NOTE

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

useMemoを理解する

Cover Image for useMemoを理解する
React

useMemo とは何か?

useMemoは値をキャッシュするためのReactHooksです。
第一引数にキャッシュしたい値を計算する関数、第二引数に依存配列を指定します。
返却値としては、第一引数で指定した関数が返すキャッシュしたい値が返ります。

const キャッシュしたい値 = useMemo(キャッシュしたい値を計算する関数, 依存配列)


注意点としては第一引数の「キャッシュしたい値を計算する関数」は引数があってはなりません。
なので、引数ありの関数をキャッシュしたい場合は以下のようにアロー関数でラップするのも一つの手です。

const cachedTax = useMemo(() => {
  CaluculateTax(price)
}, [price])

上記の場合、初回描画時にCaluculateTax関数が実行され、税額を計算します。
次回以降は初回に計算されたCaluculateTax関数の結果の値を返し続けます。そして、priceの値が変更されたときに再度CaluculateTax関数が実行され、その値がキャッシュされます。

useMemo の効果を実感する

以下のようなコードを書きました。
トップページに現在のテーマとストーリーの情報を表示しました。
そして、テーマとストーリーのジャンルを変更するためのボタンも設置しました。
今回は人為的にストーリー情報のフィルター処理を遅くするために遅延処理を入れています。

src/TopPage.js

import React, { useState } from "react";
import StoryList from "./StoryList";
import { fetchStoryList, filterStoryList } from "./fetchStoryList";

const stories = fetchStoryList();
const TopPage = () => {
  const [genre, setGenre] = useState("all");
  const [theme, setTheme] = useState("dark");
  return (
    <div style={{ backgroundColor: theme === "dark" ? "#7d7d7d" : "#ffffff" }}>
      <p>current theme is {theme}</p>
      <button onClick={() => setTheme("light")}>light</button>
      <button onClick={() => setTheme("dark")}>dark</button>
      <br />
      <p>current genre is {genre}</p>
      <button onClick={() => setGenre("all")}>ALL</button>
      <button onClick={() => setGenre("adventure")}>冒険</button>
      <button onClick={() => setGenre("suiri")}>推理</button>
      <br />
      <p>StoryList</p>
      <StoryList
        stories={stories}
        filterStoryList={filterStoryList}
        genre={genre}
      />
    </div>
  );
};

export default TopPage;


src/StoryList.js

import React from "react";

function StoryList({ stories, filterStoryList, genre }) {
  const filteredStories = filterStoryList(stories, genre);
  return (
    <>
      {filteredStories.map((story, i) => (
        <div key={i} style={{ display: "flex" }}>
          <p style={{ marginRight: "10px" }}>{story.id}</p>
          <p style={{ marginRight: "10px" }}>{story.title}</p>
          <p>{story.genre}</p>
        </div>
      ))}
    </>
  );
}

export default StoryList;


src/fetchStoryList.js

export function fetchStoryList() {
  const storiyACount = 1080;
  const storiesA = [...Array(storiyACount)].map((_, number) => {
    return {
      id: number + 1,
      title: `ワンピース第${number + 1}話`,
      genre: "adventure",
    };
  });
  const storiesB = [...Array(102)].map((_, number) => {
    return {
      id: storiyACount + number + 1,
      title: `名探偵コナン第${number + 1}話`,
      genre: "suiri",
    };
  });
  return [...storiesA, ...storiesB];
}

export function filterStoryList(stories, genre) {
  sleep(2000);
  return stories.filter((story) => {
    if (genre === "all") {
      return true;
    } else if (genre === "adventure") {
      return story.genre === "adventure";
    } else if (genre === "suiri") {
      return story.genre === "suiri";
    }
  });
}

function sleep(waitMsec) {
  var startMsec = new Date();
  while (new Date() - startMsec < waitMsec);
}


ジャンル情報を変更したときには、ストーリー情報が反映されるまで数秒時間がかかります。
しかし、テーマ情報を更新した際にも同じように反映されるまで数秒時間がかかってしまっています。これはテーマ情報を更新する際にストーリー情報のフィルター処理まで再度計算されているためです。




これを改善するためにはストーリー情報のフィルター結果をメモ化する必要があります。
src/StoryList.js

  const filteredStories = useMemo(
    () => filterStoryList(stories, genre),
    [stories, filterStoryList, genre]
  );


このように修正することで、テーマ情報更新がすぐに反映されるようになりました。


useCallback でも書けないのか?

もちろんuseCallbackでも書けます。
useMemoは関数の計算結果をキャッシュするのに対して、useCallbackは関数が再評価することを抑止する、ということです。
なので上記コードの場合はfilterStoryList関数を評価するTopPageコンポーネントにおいて、useCallbackで関数をラップする必要があります。
src/StoryList.js

export default React.memo(StoryList);

src/TopPage.js

  const cachedfilterStoryList = useCallback(
    (stories, genre) => filterStoryList(stories, genre),
    [stories, filterStoryList, genre]
  );