React でコンポーネントの再レンダーを防ぐ仕組みを理解するミニマムな例
こんにちはフロントエンドエンジニアの茶木です。
React コンポーネントのレンダーの基本を理解し、再レンダーの抑制の方法をまとめました。
先日、社内のテックシェアで React の hooks の基礎の勉強会をしたのですが、その中で useCallback
を使った、再レンダーを防ぐ仕組みを説明したかったのですが、わかりやすい例を準備できなかったのでこちらのブログでリベンジします。
React コンポーネントの再レンダーの基本
コンポーネントの再レンダーは、以下の場合に発生します。
- state に変更があった場合
- props に変更があった場合
- 親コンポーネントでレンダリングがあった場合
再レンダーが起きる例
実際の実装に近い形かつミニマムな例を考えてみました。
text input と reset を持ったフォームです。
これはちょっとありそうな例だと思います。
import React, { useState } from "react";
interface ResetProp {
reset: () => void;
}
const Reset = ({ reset }: ResetProp) => {
console.log("Reset: render");
return <button onClick={reset}>リセット</button>;
};
const Form = () => {
console.log("Form: render");
const [text, setText] = useState("");
const onReset = () => {
setPassword("");
};
return (
<>
<input
value={text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
}}
/>
<Reset reset={onReset} />
</>
);
};
export default Form;
挙動
テキスト入力もリセットボタンでテキストの削除も問題なく動作します。
しかし、テキスト入力ごとにリセットボタンコンポーネントのレンダーが起きます。
リセットボタンコンポーネントは、このレンダー前とレンダー後で見た目も機能も変わらないので再レンダーは必要ありません。これを抑制します。
再レンダー発生までの流れ
再レンダーまでの流れをまとめます。
- テキスト入力をする
- input の
onChange
がコールされる onChange
の中でsetText
がコールされるsetText
によってtext
つまり state が変更される- state の変更によって Form コンポーネントが再レンダーされる
- Reset コンポーネントの再レンダー条件がそろう
条件a. 親コンポーネントでレンダリングがあった
条件b. props に変更があった
ここで、Reset コンポーネントの再レンダーが発生する条件が2つ発生します。どちらかでも満たしていれば再レンダーが発生するので、両方とも解消する必要があります。
React.memo を 使う
React.memo
は親コンポーネントの変化を吸収します。
第一引数は、コンポーネントの props に相当します。これで条件a を解消しました。
import React, { memo } from "react";
const Reset = memo(({ reset }: ResetProp) => {
console.log("reset: rendered");
return <button onClick={reset}>リセット</button>;
});
Reset.displayName = "Reset";
Reset.displayName = "Reset";
は、memo を使うと displayName
がセットされずに環境によっては警告がでるので、セットしています。
条件bを理解する
上記の React.memo
を設定した状態で実行しても変わらず再レンダーは走ります。
これは条件b の Reset コンポーネントの props の変更が原因です。
再レンダー発生までの流れをもう一度確認すると、
- テキスト入力をする
- input の
onChange
がコールされる onChange
の中でsetText
がコールされるsetText
によってtext
つまり state が変更される- state の変更によって Form コンポーネントが再レンダーされる
- Reset コンポーネントの再レンダー条件がそろう
条件a. 親コンポーネントでレンダリングがあった
条件b. props に変更があった
5で Form コンポーネントが再レンダーされたときに、
const onReset = () => {
setPassword("");
};
onReset
が再度宣言されます。
この onReset
が Reset コンポーネントの props.reset
にセットされます。
props.reset
の値が変わり 条件b が満たされました。
useCallback を使う
onReset を固定化することで、props.reset
にセットする値が変わらないようにします。
const onReset = useCallback(() => {
setText("");
}, []);
これで、条件b を止めました。
全部のコード
以下が、Reset フォームが テキストの入力によって再レンダーされるのを抑制したコードになります。
import React, { useState, memo, useCallback } from "react";
interface ResetProp {
reset: () => void;
}
const Reset = memo(({ reset }: ResetProp) => {
console.log("reset: rendered");
return <button onClick={reset}>リセット</button>;
});
Reset.displayName = "Reset";
const Form = () => {
console.log("Form: render");
const [text, setText] = useState("");
const onReset = useCallback(() => {
setText("");
}, []);
return (
<>
<input
value={text}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setText(e.target.value);
}}
/>
<Reset reset={onReset} />
</>
);
};
export default Form;
まとめ
親要素 Form コンポーネントの再レンダー時の子要素 Reset コンポーネントの再レンダーの抑制の方法を書きました。
ただ、通常は再レンダーは React に任せておいても問題ないのでパフォーマンス上の問題がでるコンポーネントに対してのみ再レンダーの抑制は対応するのが良いと思います。