タイトルの通りです。
今行っているとあるプロジェクトでReactによるコンポーネントあたりのパフォーマンス改善について対応を行っておりました。
Reactのコンポーネントのパフォーマンス改善といえば、
shouldComponentUpdateやPureComponent(スペル合ってるかな)だったり、hooksですとReact.memoやもっと厳密に対応するなら**React.memoの第二引数(areEqual)**での対応を行うのが一般かなと思います。
そこで再レンダリング対策を行うにあたって、props.childrenでの再レンダリング対応ですこーーし悩んだので、備忘録を書いていきます。
同じくこちらで苦労した先人の対応を使っただけなので、メモ程度な感じです。先人の知恵は参考リンクに貼っておきます。
先にいっておきたい
こちら対応はしたのですが、正直ルール決めて使わないと使えないかもです。
ちゃんと使える形になったら追記という形で書いていこうかなと思います
使うコンポーネント
例えばこのようなコンポーネントがあったとします。
export const Button = memo((props) => {
return (
<div className={ props.title ? "buttonContainer" : '' }>
{
props.title ?
<div
className="buttonBalloon"
style={{
opacity: props.title ? 1 : 0
}}
>
<p>{ props.title }</p>
</div>
: null
}
<div
className={ `button${ props.type ? ` button--${ props.type }` : '' }` }
onClick={ props.onClick }
>
{ props.children }
{
props.text ? <p>{ props.text }</p> : null
}
</div>
</div>
)
})
ボタンコンポーネントの例です。
propsでボタンの文字やテキストなどもらってボタンの色を変えたり、文言を変えたり、(実際のコンポーネントはもっと色々書いてあったけど)ボタンって色々しますよね…w
で、こちらのボタンコンポーネントはprops.childrenを使っています。
再レンダリング対策というくらいなので、例えばですがchildrenの内容に変更があれば再レンダリングをする、なんてことを書きたくなってしまうわけです。
その際にここではReact.memoを使用しているので、第二引数を使って再レンダリングを防止していこうかと思います。
props.childrenを単に比較する
const areEqual = (prevProps, nextProps) => {
// これがtrueを返すときは再レンダリングされない
return (
prevProps.children === nextProps.children
)
};
export const Button = memo((props) => {
return (
<div className={ props.title ? "buttonContainer" : '' }>
{ ... }
</div>
)
}, areEqual)
childrenの変化があれば再レンダリングがされます。のイメージです。
軽ーい気持ちで比較
単に比較するだけだと常にtrueが返ってくる
logに出すと常にtrueが返ってきます。
childrenの中身をlogで出してみるとkeyとrefが常に新しいことが来ていることに気づく・・・
しかも、中が配列だったり、Objectだったり、単に文字列の場合もある。。。
先人の力を借りる
海外の方が対応してた!ので、使ってみる。
const flatten = (children, flat = []) => {
flat = [ ...flat, ...React.Children.toArray(children) ]
if (children.props && children.props.children) {
return flatten(children.props.children, flat)
}
return flat
}
export const simplify = children => {
if (typeof children === 'undefined' || children == null) return
const flat = flatten(children)
return flat.map(
({
key,
ref,
type,
props: {
children,
...props
}
}) => ({
key, ref, type, props
})
)
}
文字列以外はこれで比較できた。
2020/11/10追記
使用方法
import { simplify } from '../../service/common'
const areEqual = (prevProps, nextProps) => {
const prevChildren = JSON.stringify(simplify(prevProps.children))
const nextChildren = JSON.stringify(simplify(nextProps.children))
// これがtrueを返すときは再レンダリングされない
return (
prevChildren === nextChildren
)
};
export const Button = memo((props) => {
return (
<div>
{ ... }
</div>
)
}, areEqual)
余談
今回はReact.memoの第二引数を用いての再レンダリング対策で例を出しましたが可能なら、
公式にもあるように(React.memo – React)
デフォルトでは props オブジェクト内の複雑なオブジェクトは浅い比較のみが行われます。比較を制御したい場合は 2 番目の引数でカスタム比較関数を指定できます。
これはパフォーマンス最適化のためだけの方法です。バグを引き起こす可能性があるため、レンダーを「抑止する」ために使用しないでください。
注意
クラスコンポーネントの shouldComponentUpdate() とは異なり、この areEqual 関数は props が等しいときに true を返し、props が等しくないときに false を返します。これは shouldComponentUpdate とは逆です。
とあります。結局は複雑なareEqualだと等価性チェックでコストがかかったり、逆にバグを生んでしまう恐れがあるので、パフォーマンス向上が見込めないのであれば使わない方がいいです。
useEffect、useCallbackなど使ったり、不要なpropsを送らない等で再レンダリングを防止していった方がいいかと思います。