今回は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