EY-Office ブログ

React CompilerでuseMemoが要らなくなるらしいが、そもそも何故useMemoが必要なのでしょうか?

今回は昔話をします。次のReact、React 19に入りそうな機能は既にReact Labs: 私達のこれまでの取り組み - 2024年2月版で発表されていますが、React Server Compnent系以外で大きな項目はReact Compilerかなと思います。

React Compilerとは同ブログに、

React は state が変更された際、ときどき過剰な再レンダーを行います。React の黎明期より、このような場合の解決策は手動によるメモ化を行うことでした。現在の API においては、これは useMemo、useCallback、memo の各 API を適用して、state の変更に対する React の再レンダーの量を手動で調整することを意味します。

と書かれています。

WhyNeedUseMemo Bing Image Creatorが生成した画像を使っています

昔々、Reactはクラスで書きました

私が最初に使ったReactはバージョン0.14でした、2015年の頃です。
この時代、Reactコンポーネントはクラスで書きました。関数コンポーネントもありましたが、まだStateは扱えませんでした。

このブログで良く出てくるジャンケンアプリをクラスコンポーネント(TypeScript)で書くと次のようになります。

import React from 'react';
import { Te, ScoreType, Jjudgment } from './JyankenType';
import JyankenBox from './JyankenBox';
import ScoreBox from './ScoreBox';

type PropsType = {};
type StateType = {
  scores: ScoreType[]
};
export default class App extends React.Component<PropsType, StateType> { // ← ①
  state: StateType = {scores: []};                                       // ← ②

  pon(human: Te) {                                                 // ← ③
    const computer: Te = Math.floor(Math.random() * 3);
    const judgment: Jjudgment = (computer - human + 3) % 3;
    const score = {human: human, computer: computer, judgment: judgment};
    this.setState({scores: [score, ...this.state.scores]});        // ← ④
  };

  render() {                                                       // ← ⑤
    return (
      <>
        <h1>じゃんけん ポン!</h1>
        <JyankenBox actionPon={(te) => this.pon(te)} />           {/* ← ⑥ */}
        <ScoreBox scores={this.state.scores} />                   {/* ← ⑦ */}
      </>
    );
  }
}

最近Reactを使いだした人はクラスコンポーネントを知らないかもしれないので、簡単に解説すると

  • ① クラスコンポーネントは React.Componentクラスを継承したクラスで定義します
    • Props、Stateの型をテンプレートに指定します
  • ② Stateはクラスのインスタンス変数stateにオブジェクトで持ちます
  • ③ 内部処理で使うメソッド(関数)はインスタンスメソッドとして定義します
  • ④ Stateの更新はsetState()メソッドで行います
  • render()は描画用メソッドです、このメソッドが表示用JSXを戻します
  • ⑥ 子コンポーネントにメソッドを渡すには無名関数で渡します
  • ⑦ Stateの値の参照はインスタンス変数の参照です

クラスコンポーネントは、

  • thisがコンテキストによってインスタンスではないので、思わぬエラーになる
  • Stateが1つのオブジェクトなので、コードが判りにくくなる
  • JavaScriptの関数型言語的な側面と相性が悪い

などが、嫌われていました。

Hooksがやって来た !

2019年、Reactバージョン16.8でHooksが導入され、関数コンポーネントでステートが扱えるようになりました。

上のコードは関数コンポーネントでは以下のように書けるようになりました。現在のReactですね 👍

// ・・・ importは省略 ・・・

export default function App() {
  const [scores, setScrores] = useState<ScoreType[]>([]);   // ← ⓐ

  const pon = (human: Te) => {           // ← ⓑ
    const computer: Te = Math.floor(Math.random() * 3);
    const judgment: Jjudgment = (computer - human + 3) % 3;
    const score = {human: human, computer: computer, judgment: judgment};
    setScrores([score, ...scores]);     // ← ⓒ
  }

  return (                              // ← ⓓ
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
      <ScoreBox scores={scores} />
    </>
  );
}

さて、Stateは ⓐ で定義されています、ここでStateは試合結果の配列です。
useState()の戻り値は配列で、第1要素は現在のStateの値、第2要素はStateの値を設定(変更)する関数です。

このジャンケンアプリではジャンケンボタンを押すとpon()関数が実行されコンピューターの発生した乱数と、人間が押したボタンでジャンケンの勝負が行われます。 ⓒ でその結果が試合結果の配列に追加されStateに設定されます。

その後、適当なタイミングで<App>コンポーネントが再描画され、最新の試合を含む試合結果が<ScoreBox>で表示されます。

再描画とは

再描画とは何が起こるのでしょうか? クラスコンポーネントではrender()メソッドが実行されます。

しかし関数コンポーネントではApp関数が再実行されます。
ⓐのuseState()、ⓑのpon()関数の定義、ⓓの表示用JSX、すべてが実行されます。

  • useState()によるState管理では、Stateの値はReactライブラリー内で行われており、ⓐが再実行されたタイミングで戻り値は最新のStateになります
  • pon()関数は、再度定義されます。この関数定義にはそれほど時間がかかるとは思えませんが、0ではないと思います
  • 表示用JSXが実行されて、最新の画面が作成されます

pon()関数を再描画毎に、再度定義したくない場合にはuseMemo()の関数版である useCallback() を使い以下のように書きます。

// ・・・ importは省略 ・・・

export default function App() {
  const [scores, setScrores] = useState<ScoreType[]>([]);

  const pon = useCallback((human: Te) => {
    const computer: Te = Math.floor(Math.random() * 3);
    const judgment: Jjudgment = (computer - human + 3) % 3;
    const score = {human: human, computer: computer, judgment: judgment};
    setScrores(s => [score, ...s]);       // ← ⓒ
  }, []);

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
      <ScoreBox scores={scores} />
    </>
  );
}

ⓒの部分はpon()関数がscores State値に依存しないように、setScrores()の引数を無名関数にしています。

まとめ

Hooksの登場により、Reactコンポーネントは現在のように関数で書けるようになりthisの呪いから解放されました。👍
しかし、クラスコンポーネントでは起きなかった再処理が起きてしまう副作用も抱え込んでしまいました。 これを解決するのがReact Compilerです。

React Compilerが導入されると、直前のuseCallback()を使わなくても良くなるはずです。
詳しくは React Will Be CompiledHow React Forget will make React useMemo and useCallback hooks absolutely redundant を読んでください(React Compilerは以前はReact Forgetと呼ばれていたようです)。

React Will Be Compiledには以下のように書かれていました。

  • The class components era (no primitive for abstraction)
  • The Hooks era (we need to memoize)
  • The compiled era (auto-memoization)

React19で、Reactは新しい時代に入るようです。😃

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ