たとえばAtomic Designを採用してこんなシンプルなコンポーネントを作ったとします
const Text = (props) => <p>{props.val}</p>
そして、プログレスバーを表示するコンポーネントと同じコンポーネントに置きます
class Progress React.Component {
render(){
return (
<React.Fragment>
...
<Text val="処理中です" />
<ProgressBar percent={this.props.percent} />
...
</React.Fragment>
)
}
}
“処理中です” という文字とプログレスバーが表示されます。
意図通りに動いてくれます。
なにも問題なさそうに見えます
しかし、このコードだと this.props.percent が変更されるたびに
Progress
や ProgressBar
だけでなく、Text
も再描画されます。
this.props.percentが32回変更された場合には "処理中です"
という 文字列しか渡していない Text
も32回再描画されます。
同じように、親やさらにその上のコンポーネントが更新されると、子・孫・曾孫・玄孫にあたる Text
もrenderが走ってしまうわけですね
「ん? まって。なんで同じpropsを渡してrenderされるの・・・?」
確かに、同じpropsを渡しているならrenderなんて走らなくていいはず。
ましてや渡しているのはただの文字列。
なんで同じ文字列でrenderしちゃってるのでしょう。
同じ文字列で何度もrenderされる理由
ここで、Textがどんなpropsを受け取ってるのか見てみましょう
{ val: "処理中です" }
さて、this.props.percent が変更されました。
次に受け取るpropsはこんな感じ。
{ val: "処理中です" }
ここで、F12を押して開発者ツールのコンソールの中に、以下をコピペしてEnterを押してみてください
{ val: "処理中です" } === { val: "処理中です" }
JavaScriptだと { val: "処理中です" }
と { val: "処理中です" }
は別のものとして認識されてしまいます。
オブジェクト同士を比較するときに同一のインスタンスかどうかで判断するため、falseが返ります。
渡されたpropsが前に渡されたものと異なるオブジェクト → じゃあ、またrenderしなきゃ ってなってるわけです。
や、待ってくれ。私がしてほしいのは
{ val: "処理中です" } === { val: "処理中です" }
ではなく
val === val
だ。 ちゃんと中身を見て、同じかどうか判別してほしい。
え、これどうしたら無駄なrender防げるの・・・?
解決法
そんな人のために React.memo
と React.PureComponent
が用意されました。
Reactの16.6以降のバージョンなら両方使えるようになっています。
使い方は簡単
React.memoは
const Text = (props) => <p>{props.val}</p>
↓
const Text = React.memo((props) => <p>{props.val}</p>)
React.PureComponent は
class Progress React.Component {
render(){
...
}
}
↓
class Progress React.PureComponent {
render(){
...
}
}
とするだけ。
そうすると
{ val: "処理中です" } === { val: "処理中です" }
ではなく
val === val
で比較してくれるようになります。
やったね!
気を付けないといけないこと
で、終わってくれればいいんですが、React.memo
と React.PureComponent
にも問題点はあります。
{ val: "処理中です" } === { val: "処理中です" }
を
val === val
にするということは、propsをたくさん渡すコンポーネントの場合、
{ v1:"a", v2: "b", ..., v1000: "zzz" } === { v1:"a", v2: "b", ..., v1000: "zzz" }
を
v1 === v1
v2 === v2
...
v1000 === v1000
といちいち展開して比較して、ってやってしまうわけです。
うっかりすると、renderするコストよりも比較するコストの方が重くなったりするので、
適宜計測して、どちらがよりコストが低いかを見ておいてください。
また、比較してくれるのは1段目だけです。
<Text val="処理中です" style={{ color:"red" }} />
とかしてしまうと、propsは
{ val: "処理中です", style: { color:"red" } }
となって
val === val // true
{ color:"red" } === { color:"red" } // false
という比較になり、どこかで変更があるたびに再度renderされてしまいますのでご注意を。
無駄にrenderされていないかを知るためには
why-did-you-render
というライブラリがオススメ。
https://github.com/welldone-software/why-did-you-render
無駄にrenderされている場合にconsoleへ表示してくれます。
導入も簡単なので、試してみてください。