Reactの再レンダリングの契機・タイミング – React.memoで不要なレンダリングを防ぐ

今回はReactの再レンダリングの契機・タイミングと、React.memoで不要なレンダリングを防ぐ方法について書いていきます。

Reactの再レンダリング契機・タイミング


公式ドキュメントの通り、Reactがレンダリングされるタイミングは2つあります。

  • コンポーネントの初回レンダリング
  • コンポーネント(またはその親コンポーネントのいずれか)の state の更新

https://ja.react.dev/learn/render-and-commit

すなわち、以下のようにParentComponentの下にChildComponentがあり、ParentComponentのstateが更新されたら、本来countとは無関係のChildComponentが再レンダリングされてしまいます。

import { useState } from "react"

function ChildComponent() {
  return (
      <p>This is ChildComponent</p>
  )
}

export default function ParentComponent() {
    const [count, setCount] = useState(0)

    return (
        <div>
            <p>Count: {{ count }}</p>
            <button onClick={() => setCount(count+1)}>Add</button>
            <ChildComponent />
        </div>
    )
}

不要な再レンダリングを防ぐにはReact.memoを使う


React.memoを使用して適切な条件を指定することで、Propsに変化がない場合の再レンダリングを防げます。

https://react.dev/reference/react/memo

React.memoは更新前後のPropsの同値判定を行い、変更がなければ再レンダリングを行わないようにできます。

import React from "react"

function ChildComponent() {
  return (
      <p>This is ChildComponent</p>
  )
}

// React.memoを指定した場合
// 第二引数の同値判定を明示的に指定していないパターン
export default React.memo(function ChildComponentWithMemo() {
    return (
        <p>This is ChildComponent</p>
    )
})

同値判定は明示的に指定しない限りはObject.isが用いられています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures

Propsがない場合&Propsがプリミティブ型のみの場合、プリミティブではない場合でそれぞれアプローチが異なるのでそれぞれ見ていきましょう。

また、今回の内容を私のリポジトリにまとめました。コミットを切り替えることで挙動の差分の確認ができます。

https://github.com/pei223/react-re-rendering-sample/tree/main

Propsがない場合はReact.memoの指定だけで良い

前述した通り、React.memoのPropsの同値判定は明示的に指定がない限り、Object.isが用いられます。

https://ja.react.dev/reference/react/memo#parameters

React.memoを使用すれば、Propsがない場合は同値判定がtrueになるため初回レンダリング以降で再レンダリングがされなくなります。

以下のようにReact.memoを設けると、ParentComponentのcountが更新されても再レンダリングされなくなります。

import React from "react"

export default React.memo(function ChildComponent() {
    return (
        <p>This is ChildComponent</p>
    )
})
import { useState } from "react"
import ChildComponent from "./ChildComponent"

export default function ParentComponent() {
    const [count, setCount] = useState(0)

    return (
        <div>
            <p>Count: {{ count }}</p>
            <button onClick={() => setCount(count+1)}>Add</button>
            <ChildComponent />
        </div>
    )
}

Propsがプリミティブ型のみの場合もReact.memoの指定だけで良い

Propsがプリミティブ型のみの場合はPropsがない場合と同様です。

React.memoのPropsの同値判定は明示的に指定がない限りObject.isが用いられ、プリミティブ型であれば同じ値ならtrueが返るため、同値判定を書かなくても不要な再レンダリングがされなくなります。

https://ja.react.dev/reference/react/memo#parameters

import React from "react"

type Props = {
    message: string
}

export default React.memo(function ChildComponentWithStringProps({ message }: Props) {
    return (
        <p>This is ChildComponentWithStringProps. message: {{message}}</p>
    )
})
import { useState } from "react"
import ChildComponentWithStringProps from "./ChildComponentWithStringProps"

export default function ParentComponent() {
    const [count, setCount] = useState(0)

    return (
        <div>
            <p>Count: {{ count }}</p>
            <button onClick={() => setCount(count+1)}>Add</button>
            <ChildComponentWithStringProps message="From ParentComponent" />
        </div>
    )
}

Propsがプリミティブではない場合はReact.memoの第二引数の指定が必要

今までの例では、React.memoのデフォルトの同値判定処理であるObject.isで事足りていました。

https://ja.react.dev/reference/react/memo#parameters

ただ、プリミティブではないObject型では、参照先が同じでなければ全く同じ値であってもObject.isはfalseになるため再レンダリングされてしまいます。

下記の例だと、レンダリングのたびにChildComponentWithObjectPropsに渡すオブジェクトが生成されるため、同じ値であってもObject.isでtrueになりません。

そのため、React.memoを指定していてもParentComponentのcount stateが変わると毎回レンダリングされてしまいます。

mport React from "react"

type UserData = {
    name: string
    age: number
}

type Props = {
    data: UserData
}

export default React.memo(function ChildComponentWithObjectProps({ data }: Props) {
    return (
        <div>
            <h3>This is ChildComponentWithObjectProp</h3>
            <div>Name: {data.name}</div>
            <div>Age: {data.age}</div>
        </div>
    )
})
import { useState } from "react"
import ChildComponentWithObjectProps from "./ChildComponentWithObjectProps"

export default function ParentComponent() {
    const [count, setCount] = useState(0)

    return (
        <div>
            <p>Count: {{ count }}</p>
            <button onClick={() => setCount(count+1)}>Add</button>
            <ChildComponentWithObjectProps data={{name: "Test", age: 12}} />
        </div>
    )
}

そのような場合は、以下のようにReact.memoの第二引数に同値判定処理を明示的に指定してあげる必要があります。

毎回全フィールドの同一判定を書くのは面倒で、入れ子になるとさらに面倒になるのでlodashのisEqualとか使うと良いかと思います。

https://lodash.com/docs/#isEqual

mport React from "react"

type UserData = {
    name: string
    age: number
}

type Props = {
    data: UserData
}

// 第二引数の同値判定を明示的に指定しているパターン
export default React.memo(function ChildComponentWithObjectProps({ data }: Props) {
    return (
        <div>
            <h3>This is ChildComponentWithObjectProp</h3>
            <div>Name: {data.name}</div>
            <div>Age: {data.age}</div>
        </div>
    )
},
// 更新前後のpropsの同値判定
function(prev: Props, next: Props) {
    return prev.data.name === next.data.name && prev.data.age === next.data.age
})

また、Objectの参照が同じなら再レンダリングは走らないので、data Propsの値をParentComponentでstateとして保持することで再レンダリングを防ぐことができます。

私のリポジトリに例があるのでよければ見てみてください。

https://github.com/pei223/react-re-rendering-sample/commit/6598cb5ef0bb61f100cd67733cb99c56a2b69f7d

コメントを残す

メールアドレスが公開されることはありません。