新しい記事もよろしくやで!
→ハスケル子「タグごとに色がついてたらいいのにな…」
38歳無職ワイ
ワイ「(カタカタカタカタ・・・ッターン!)」
娘(5歳)「パパ、今日は何してるの?」
ワイ「今日はな、むかしWordPressで作った自分用TODOリストの」
ワイ「デザインをリニューアルしてんねん」
よめ太郎「(そんなことより職を探せや)」
娘「へぇ〜」
娘「WordPressってことは、PHPを書いてるの?」
ワイ「いや、ちゃうで」
ワイ「リニューアル後は、フロント部分をReactで実装しようと思ってな」
ワイ「そこで、WordPressをREST APIモードで使うことにしたんや」
ワイ「つまり、WordPressを管理画面つきAPIみたいに使うってことや」
娘「要は、WordPressをヘッドレスCMSとして使うんだね」
ワイ「ヘッドレス・・・?」
ワイ「ちゃうちゃう、管理画面つきAPIや」
娘「・・・まぁいいや」
娘「とにかく、APIからTODOリストのデータを取得して」
娘「それをReactで表示したいわけだね」
ワイ「そういうことや」
現在の進捗状況
娘「それで、フロントの実装はどこまでできたの?」
ワイ「ほぼ完成してるで」
ワイ「↓こんな感じや」
よめ太郎「(酒ばっかりやないかい・・・)」
ワイ「だいぶ下のほうに『職を探す』っていうタスクも」
ワイ「ちゃんと入ってるで!」
よめ太郎「(ちゃんととは・・・)」
娘「へぇ〜」
娘「ちょっと触ってみてもいい?」
ワイ「もちろんええで」
娘「(ポチポチポチ・・・)」
娘「う〜ん・・・」
ワイ「え、どしたん」
娘「パパのReact、めっちゃ遅いね!」
ワイ「え、マジ・・・?」
娘「なんか動きが重いよ?」
ワイ「え、そんなことないやろ・・・」
ワイ「(ポチポチポチ・・・)」
ワイ「あれ、ほんまや」
ワイ「検索フォームにタスク名を入力するときに、なんかモタついてんな」
ワイ「キーを叩いてから文字が表示されるまでの間に、タイムラグが1秒近くあるで」
ワイ「何やこれ・・・」
娘「パパ、もしかしたらコンポーネントたちが無駄に再レンダーされてるのかもよ」
娘「そんな時は、React Developer Toolsを使って再レンダー状況を可視化してみようよ」
ワイ「そ、そんな機能あんの?」
娘「あるよ」
- ChromeにReact Developer Toolsという拡張機能を追加する
- Chrome Devtoolsを開く
- Componentsタブを開く
- 歯車アイコンをクリック
-
Highlight updates when components render.
にチェックを入れる
娘「手順としては↑こんな感じだね」
ワイ「なるほど・・・」
ワイ「↑ここにチェックを入れるんやな!」
娘「そうだね!」
さっそくレンダー状況を見てみる
ワイ「何やこれ」
ワイ「タスク名の入力フォームに文字を1つ入力するたびに」
ワイ「100件くらいあるTODOリスト全体が再レンダーされてるやん」
ワイ「まだ検索ボタンを押してもないのに・・・!」
娘「そうだね・・・」
ワイ「検索ボタンを押して、そんで検索結果が変わったことでリストが再レンダーされるならまだしも」
ワイ「文字を1つ入力したり消したりするたびにリスト全体が再レンダーされるんかい」
ワイ「何やそれ・・・」
娘「たぶん、タスク名のinput
タグにイベントハンドラが設定されてて」
娘「文字を入力するたびに何かページの状態が変わって」
娘「それでページが再レンダーされてるんじゃない?」
娘「ページ内でuseState
とか使ってるよね?」
ワイ「おお、それはその通りや」
const [taskName, setTaskName] = useState('')
ワイ「↑この、useState
で作ったsetTaskName
っていう関数があってな」
ワイ「その関数が、タスク名の入力フォームに文字を入力するたびに実行される仕組みやねん」
娘「なるほどね」
娘「その時に、TODOリストも毎回ぜんぶ再レンダーされちゃってるんだね」
React.memo
で、コンポーネントをメモ化しよう
娘「パパ、そんな時はReact.memo
を使って」
娘「TODOリストのコンポーネントをメモ化してみようよ」
ワイ「ど、どうやんの・・・?」
娘「ええと・・・」
- const TodoList: React.FC<Props> = ({ list }) => {
+ const TodoList: React.FC<Props> = React.memo(({ list }) => {
return (
<ul>
{list.map((task) => (
<TodoItem task={task} key={task.id} />
))}
</ul>
)
- }
+ })
+ TodoList.displayName = 'TodoList'
娘「↑こうだね」
ワイ「おお、React.memo
っていう関数に」
ワイ「TodoList
コンポーネントの中身をぶち込んであげるだけなんやな」
娘「うん!」
よめ太郎「(前々から思ってたけど)」
よめ太郎「(引数をぶち込むって何やねん・・・)」
モタつきは解消されたのか
ワイ「おお、TODOリストがピカピカせんようになっとるわ」
ワイ「文字の入力が重たい感じもなくなっとる」
娘「よかったね!」
ワイ「・・・これはどういうことなん?」
娘ちゃんによる解説
娘「えっとね、今までは」
React「お、検索フォームに文字が入力されたで!」
React「ほな、ページの状態が変わったことやし、再レンダーや!」
React「TodoList
コンポーネントも再レンダーや!」
娘「↑こんな感じだったんだけど」
娘「コンポーネントをメモ化すると・・・」
React「お、検索フォームに文字が入力されたで!」
React「でも、TODOリストの中身は変わってないから」
React「TodoList
コンポーネントはレンダーし直す必要ないか・・・」
React「さっきメモしといた結果を再利用しとこか」
娘「↑こんな感じだね」
よめ太郎「(なんでReactも関西弁やねん)」
ワイ「なるほどなぁ」
ワイ「useState
で管理している状態が、文字入力によって変わったとしても」
ワイ「TODOリストに関係のない変更だけが起こっていた場合には」
ワイ「再レンダーをスキップしてくれるんやね」
娘「そういうことだね!」
ワイ「ありがとうやで、娘ちゃん!」
しかし30分後・・・
ワイ「こ、今度はタスクの削除機能を実装したら」
ワイ「また画面の動きが遅くなってもうた・・・」
娘「パパ、今度はどうしたの?」
ワイ「おお、娘ちゃん・・・」
ワイ「あのな・・・」
+ const onDelete = (taskId: number): void => {
+ /* 省略 */
+ }
return (
<>
<h1>TODOリスト</h1>
<form>
{/* 省略 */}
</form>
- <TodoList list={todoList} />
+ <TodoList list={todoList} onDelete={onDelete} />
</>
)
ワイ「↑こんな感じで、タスク削除用の関数を作って」
ワイ「それをTodoList
コンポーネントに渡すようにしただけなんや・・・」
ワイ「それなのに・・・」
ワイ「また文字入力のたびにTODOリスト全体がピカピカしてんねん・・・」
娘「そんな時は、またメモ化だね!」
useCallback
で、関数もメモ化しよう
- const onDelete = (taskId: number): void => {
+ const onDelete = useCallback((taskId: number): void => {
setTodoList((prevList) => {
return prevList.filter((todoItem) => todoItem.id !== taskId)
})
- }
+ }, [])
ワイ「おお、今度はonDelete
関数の中身を」
ワイ「useCallback
というやつにぶち込んだるわけか」
娘「そうそう」
娘「これでもう一度、画面を触ってみて?」
ワイ「おお、チカチカしないし、サクサク入力できとるわ」
ワイ「・・・でも、これ何でなん?」
ワイ「TodoList
コンポーネントに渡すonDelete
関数は、毎回おんなじ関数なんやから」
ワイ「TodoList
は再レンダーされないはずちゃうの・・・?」
娘「それはね・・・」
const onDelete = (taskId: number): void => {
/* 省略 */
}
return (
<>
<h1>TODOリスト</h1>
<form>
{/* 省略 */}
</form>
<TodoList list={todoList} onDelete={onDelete} />
</>
)
娘「↑ここで、onDelete
関数を作って」
娘「TodoList
コンポーネントに渡してるでしょ?」
娘「そこが問題・・・というかポイントなの」
娘「つまり・・・」
React「よっしゃ、
onDelete
関数を作って」
React「TodoList
コンポーネントにぶち込むでぇ!」
娘「↑こんな感じで、作りたての新しい関数をTodoList
コンポーネントに渡す」
娘「そういうことになっちゃうの」
ワイ「作りたて・・・」
ワイ「そう言われてみれば、アロー関数式で関数を作って」
ワイ「それをTodoList
コンポーネントに渡してるわけやもんな」
娘「そう」
娘「だから・・・」
React「さっきまでとは違う、今作ったばかりの
onDelete
関数を渡すから」
React「TodoList
コンポーネントも再レンダーや!」
娘「↑こんな感じになっちやうの」
ワイ「毎回新しい関数が作られて、別物扱いになってまうわけか」
ワイ「つまり、ReactはObject.isによる比較アルゴリズムを使用しているわけやな・・・」
よめ太郎「(いや、そこは急に理解早いんかい)」
娘「そういうことだね」
娘「そして、useCallback
を使うと」
ワイ「この関数は毎回作り直さないで、前回と同じのを使ってな!」
娘「ってことをReactに伝えられるわけだね」
ワイ「ふーん、そうするとTodoList
コンポーネントが再レンダーされなくなるん?」
娘「そうだよ」
React「TODOリストの内容も
onDelete
関数も、前回と変わってへんな」
React「ほな今回はTodoList
コンポーネントの再レンダーはスキップや!」
娘「↑って感じになるの」
ワイ「なるほどなぁ」
ワイ「ちなみにuseCallback
の第二引数の配列、これは何なん?」
娘「これは・・・」
ワイ「
hoge
っていう変数の中身が変わった場合には」
ワイ「前回と同じ関数を使いまわさずに、新しく作ってな!」
娘「↑的なことをReactに伝えるときのための配列だね」
ワイ「ほぇ〜」
ワイ「そうやって新しく関数が作り直された場合には」
ワイ「その関数を受け取ったコンポーネントは再レンダーされるわけか」
娘「そうそう」
ちなみに
娘「ちなみに、useCallback
を使わないでも」
娘「ページコンポーネントの外で関数を定義するだけで」
娘「再レンダーを抑止できる場合もあるよ!」
ワイ「そうかそうか」
ワイ「ページコンポーネントの外側で定義された関数は」
ワイ「ページが再レンダーされても変わらへんわけやな」
ワイ「まぁ、とにかく激重サイトにならんくてよかったわ!」
まとめ
- Reactアプリの動作が重い時は、React Developer Toolsで再レンダー状況をチェックしてみよう
-
React.memo
を使ってコンポーネントをメモ化し、無駄な再レンダーを抑止しよう - メモ化したコンポーネントに渡すための関数も
useCallback
でメモ化しておこう
ワイ「↑ってことやな!」
娘「そうだね!」
ワイ「いやー、娘ちゃんのおかげで良いTODOリストができそうやわ・・・!」
娘「あっ!」
娘「・・・パパ・・・!」
ワイ「なんや?」
娘「大変なことに気づいたんだけど・・・」
娘「このTODOリスト、どこからタスクを追加するの?」
ワイ「あ・・・」
ワイ「タスクの作成機能を作るの、忘れてたわ・・・!」
娘「・・・」
ワイ「で、でも大丈夫や!」
- タスク追加機能を実装する
ワイ「↑っていうタスクを、このTODOリストに追加しておけばええねん!」
ワイ「そしたらもう忘れへん!」
娘「で、そのタスクはどこから追加するの?」
ワイ「それな」
〜おしまい〜
参考文献
新しい記事もよろしくやで!
補足