Edited at

本当は怖いReact.memo

React v16から登場したReact.memoの話です

https://reactjs.org/blog/2018/10/23/react-v-16-6.html

FCで shouldComponentUpdate 相当のことができて超便利!

でもちょっと待って!そのReact.memo、使わない方が良いかもしれません…


React.memoの方がFCよりも重くなってしまうこともある

Shallow Equal するよりそのままポンッと再レンダリングした方がコストが低い場合があります

特にコンポーネント内で大して処理してない場合(JSX返すだけとか)は使わなくても良いのかも?(パフォーマンス詳しい人教えてください)

もちろんですがPropsに何も渡してないコンポーネントは やってもやらなくてもいいと思います

12/3 追記

Shallow Equal のコストのほうが重いので、後述する第二引数で再レンダリングを永久に拒否しない限り使わない方が良いと思います


Shallow Equal で比較される

PureComponentに関して造詣が深い方は読み飛ばしてください

そもそも普通の ComponentFC はPropsが変更されると毎回再レンダリングされます(だったはず)

でレンダリングを抑えるのに shouldComponentUpdate メソッドを自分で作って「何がどうなってたらこのコンポーネントを再レンダリングする」って実装していました

しかしそれも一々実装するのが面倒なので、 shouldComponentUpdate を自動でやってくれる PureComponent なるものが出ました

この PureComponentshouldComponentUpdateShallow Equal で前回のPropsと今回のPropsを比較し再レンダリングするかどうかを判断しています

(参考) https://qiita.com/wifecooky/items/23fd1da041f707c1b78b

その PureComponentFC でも使えるようにしたのが React.memo です

ということは、 React.memo はデフォだと例外なく Shallow Equal で判断されることになります

Shallow Equal はネストの深いPropsオブジェクトは苦手です

ここで覚えてほしいのは、 何でもかんでもReact.memoにすればいいというわけではない ということです

ネストの深いPropsオブジェクトを渡しているコンポーネントが意図しない再レンダリング拒否を起こし、全く更新されなくなる可能性があります


意外と知らない第二引数の使い方

React.memoには第二引数が存在します

これめちゃめちゃ重要です

ここに簡単な message を受け取ってそれを返却するコンポーネントがあります

const Component = React.memo(({ message, children, ...props }) => (

<div>{message}</div>
))

さて、よく導入記事とかで見るReact.memoはこんな感じですがこれを俺色にしてみたいと思います

const Component = React.memo(({ message, children, ...props }) => (

<div>{message}</div>
), (prevProps, nextProps) =>
prevProps.message === nextProps.message
)

第二引数が増えました!

React.memoは第二引数に 再レンダリングするか否か を判断する関数を入れることができます

…もう一度言います

再レンダリングするか否か を判断する関数を入れることができます

あれ?でも先程「React.memoは Shallow Equal される」って言ってましたよね?

それは嘘です 正しくは「(第二引数に何も入ってない場合のみ) Shallow Equal される」だったのです!

もっと噛み砕いて言うと、React.memoは「 shouldComponentUpdate を実装できて、デフォは Shallow Equal される」機能です

ついでにもう一つ注意ですが、 React.memoの第二引数は shouldComponentUpdate と違って レンダリングしない時にtrue になります

再レンダリング
shouldComponentUpdate
React.memo


true
false

×
false
true

表にするとこうなります

React.memoは shouldNotComponentUpdate って覚えておくと良いと思います

(参考) https://reactjs.org/docs/react-api.html#reactmemo

この第二引数によって Shallow Equal のコストについて考えなくても良くなるので利用の幅がグッと広がると思います


実装例

コンソール見ながら動かしてみてください

デモ => https://codesandbox.io/s/qxmw7rl2ww

import * as React from "react";

import * as ReactDOM from "react-dom";

type Tweet = {
author: string;
body: string;
};

type State = {
tweets: Tweet[];
newTweet: Tweet;
};
const initialState: State = {
tweets: [
{
author: "ぼく",
body: "おすしたべたい"
}
],
newTweet: {
author: "",
body: ""
}
};

class Top extends React.Component<{}, State> {
readonly state = initialState;

changeNewTweet = (t: Tweet) => {
this.setState(state => ({
...state,
newTweet: t
}));
};

postNewTweet = (t: Tweet) => {
if (t.author === "" || t.body === "") return;
this.setState(state => ({
...state,
newTweet: {
author: "",
body: ""
},
tweets: [...state.tweets, t]
}));
};

render() {
const { tweets, newTweet } = this.state;
return (
<main>
<h1>Twitterみたいなやつだよ</h1>
<NewTweetForm
tweet={newTweet}
onChangeTweet={this.changeNewTweet}
onSubmitTweet={this.postNewTweet}
/>
<TweetList tweets={tweets} />
</main>
);
}
}

// tweetsが変わらなかったら再レンダリングしなくていいのでmemoする
const TweetList = React.memo<{ tweets: Tweet[] }>(
({ tweets, ...props }) => {
console.log("TweetList is rendered.");
return (
<section {...props}>
<h1>一覧</h1>
<div>
{tweets.map((t, i) => (
<TweetListItem tweet={t} key={i} />
))}
</div>
</section>
);
},
(p, n) => p.tweets === n.tweets
);

// tweetが変わらなかったら再レンダリングしなくていいので
const TweetListItem = React.memo<{ tweet: Tweet }>(
({ tweet, ...props }) => {
console.log("TweetListItem is rendered.");
return (
<article {...props}>
<h1>{tweet.author} さんの投稿</h1>
<p>{tweet.body}</p>
</article>
);
},
(p, n) => p.tweet === n.tweet
);

// これもtweetが変わらなかったら再レンダリングしなくていいので(略
type NewTweetFormProps = {
tweet: Tweet;
onChangeTweet: (t: Tweet) => void;
onSubmitTweet: (t: Tweet) => void;
};
const NewTweetForm = React.memo<NewTweetFormProps>(
({ tweet, onChangeTweet, onSubmitTweet }) => {
console.log("NewTweetForm is rendered.");
return (
<section>
<h1>新規投稿</h1>
<form
onSubmit={e => {
e.preventDefault();
onSubmitTweet(tweet);
}}
>
<label>
<p>投稿者</p>
<input
type="text"
max="24"
value={tweet.author}
onChange={e =>
onChangeTweet({ ...tweet, author: e.target.value })
}
/>
</label>
<label>
<p>本文</p>
<textarea
value={tweet.body}
onChange={e => onChangeTweet({ ...tweet, body: e.target.value })}
/>
</label>
<div>
<button type="submit">投稿</button>
</div>
</form>
</section>
);
},
(p, n) => p.tweet === n.tweet
);

ReactDOM.render(<Top />, document.querySelector("#app"));