エンジニアを目指す日常ブログ

日々勉強したことのメモ。独学ですので間違っていたらコメント等で教えてください。

【React】レンダリングを制御するための関連知識メモ

はじめに

Reactではレンダリングの動作を理解することがとても重要で、レンダリングを制御するための機能が様々用意されている。use~系が多くて難しいので、基本的なところをメモしておく。対象は以下4つとする。

useEffect

副作用を制御するuseEffect。

レンダリングに関わる関数・メソッド

以下、メモ化三兄弟。

  • memo():コンポーネントのメモ化
  • useCallback():関数のメモ化
  • useMemo():変数(関数の処理結果)のメモ化

Reactで再レンダリングが行われる条件

そもそもReactでは、以下の条件で再レンダリングが行われる。

useEffect()

useEffectは以下2つの目的で使うものらしい。

  • ある処理を、特定の値が変化した時のみ動作させることができる
  • レンダリングが完了した後に処理を実行させることができる

公式ページには以下のように記載がある。

副作用を有する可能性のある命令型のコードを受け付けます。

DOM の書き換え、データの購読、タイマー、ロギング、あるいはその他の副作用を、関数コンポーネントの本体(React のレンダーフェーズ)で書くことはできません。それを行うと UI にまつわるややこしいバグや非整合性を引き起こします。

代わりに useEffect を使ってください。useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。副作用とは React の純粋に関数的な世界から命令型の世界への避難ハッチであると考えてください。

デフォルトでは副作用関数はレンダーが終了した後に毎回動作しますが、特定の値が変化した時のみ動作させるようにすることもできます。

フック API リファレンス – React

副作用を含む処理はuseEffectの中に書くように…と言っている。(難しい)

副作用とは、alertや、APIでのデータ取得や、console.logなどがある。

使い方

useEffect ( () => {処理}, [依存配列] )

依存配列にstateを設定すると、そのstateが更新されたときのみ第一引数の処理を実行する。依存配列に[]を設定した場合は、初期レンダリング完了時のみ処理を実行する。

利用例

特定の値が変化した時のみ動作させる目的での使い方を記載しておく。 関心を分離するために複数の副作用を使うという例が公式ページにも記載されている。

副作用フックの利用法 – React

たとえば値をカウントアップしていって、値が10で割り切れる数になった際にカウント2(10の位をカウントアップするプログラムを作ってみる。

カウントアップのイメージ
カウントアップのイメージ

App.tsx

import { useEffect, useState } from "react";

export const App = () => {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);

// count1が更新された場合のみ以下の処理を実行する
    useEffect(() => {
        if (count1 > 0 && count1 % 10 === 0) {
            setCount2((count2) => count2 + 1);
        }
    }, [count1]);

    const onClickCountUp = (e: React.MouseEvent<HTMLButtonElement>) => {
        setCount1((count1) => count1 + 1);
    };

    return (
        <>
            <button onClick={onClickCountUp}>カウントアップ</button>
            <p>{`カウント:${count1}  カウントの10の位:${count2}`}</p>
        </>
    );
};

もしuseEffectを使わなかった場合、以下のエラーが出ることになる。

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

count1が10になった際、count2がカウントアップされて1になる。count2が変更されたので再レンダリングされる。count1は10なのでまたcount2がカウントアップされる。count2が変更されたので・・・と、count2のカウントアップが繰り返されるためである。

memo()

コンポーネントをメモ化してくれる関数。コンポーネントに対して設定する。親コンポーネントが再レンダリングされても、propsに変更がない限り再レンダリングされないようにする

使い方

コンポーネント側の関数をmemo()で囲む。(記載例はtype-script。)

Child.tsx

// インポートが必要
import { memo, VFC } from "react";

type Props = {
    text: string;
};

// 関数全体をmemoで囲む
export const Child: VFC<Props> = memo((props) => {
    const { text } = props;
    return (
        <div>{text}</div>
    );
});

このときChildコンポーネントは、呼び出す側の親コンポーネントが再レンダリングされたとしても、propsであるtextが更新されない限りは再レンダリングされなくなる。

useCallback()

第一引数に渡している関数をメモ化してくれる。 第二引数で、関数を再定義する条件を指定する(useEffectと同様)。

どんなときに使うか

基本的には、子コンポーネントのpropsとして関数を渡している場合に利用する。

memo()で子コンポーネントをmemo化しても、propsに関数がある際、親コンポーネントが再レンダリングされた際に関数が再定義されることで、結局propsが変更された扱いとなり、子コンポーネントが再レンダリングされてしまう。

そうなるのを防ぐために、子コンポーネントにpropsで渡している関数をuseCallback()でメモ化しておく必要がある。

逆に、子コンポーネントをmemo化していないと意味が無いので注意。

利用例

コンポーネント

App.tsx

import { useCallback, useState } from "react";
import { Child} from "./Child";

export const App = () => {

    const [text, setText] = useState("");
    const [count, setCount] = useState(100);
    const onClickInit = useCallback(() => setCount(0), []);

    return (
        <>
            <p>{`カウント:${count}`}</p>
            <Child onClick={onClickInit} />
        </>
    );
};

コンポーネント

Child.tsx

import { memo, VFC } from "react";

type Props = {
    onClick: () => void;
};

// memo化しているのでonClickが再定義されなければ再レンダリングされない
export const Child: VFC<Props> = memo((props) => {
    const { onClick } = props;
    return (
        <>
            <button onClick={onClick}>カウント初期化</button>
        </>
    );
});

useMemo()

変数をメモ化してくれる関数。 実践で利用したことはまだ無い。

使い方

変数の導出処理が重い場合、再レンダリングするたびに計算すると大変なので、初回レンダリング時にのみ計算するよう設定する。

第二引数の設定は、useEffectやuseCallbackと同じルール。

 const value = useMemo(() => (1+2+3+4+…重い計算処理…) , []);

実際に使ってみた例は以下となる。

import { useMemo, useState } from "react";

export const App = () => {
    const calc = () => {
        let num = 0;
        for (let i = 1; i < 100000000; i++) {
            num += i;
        }
        console.log("重い計算処理calc()を実行");
        return num;
    };

    // 変数valueの計算が重い場合
    const value = useMemo(() => calc(), []);

    // stateを更新するボタン
    const [state, setState] = useState(0);
    const onClickState = () => setState((state) => state + 1);

    return (
        <>
            <button onClick={onClickState}>stateを更新するボタン</button>
            <p>{`State:${state}   value:${value}`}</p>
        </>
    );
};

重要なのは

const value = useMemo(() => calc(), []);

の部分で、このように書くことでvalueの計算が初回レンダリング時のみで済む。

普通に

const value = calc();

と書くと、ボタンを押してstateを変更するたびにcalc()が実行されてしまうことがコンソールログから確認できる。

おわりに

ひとまずメモ化三兄弟を整理できた。useEffectは奥が深く理解しきれていないので、理解し次第追記する。