React の再レンダリングを理解する
投稿日: 2025年04月18日
React は現代の Web 開発において最も人気のあるライブラリの一つであり、その中核となる機能は「コンポーネント」と「レンダリング」のシステムです。 この記事では React の再レンダリングの仕組みを深く掘り下げていきたいと思います。(間違いなどがあれば指摘していただけるとありがたいです!)
React の再レンダリングとは何か
再レンダリングが発生するタイミング
再レンダリングの問題点
再レンダリングを最適化する
まとめ
React におけるレンダリングとは、コンポーネントが実行されて、React 要素(Virtual DOM)を生成するプロセスです。再レンダリングとは、コンポーネントが何らかの理由で再度実行され、新しい React 要素ツリーを生成することを指します。
React の重要なポイントは、レンダリングと実際の DOM 更新が分離されているということです。レンダリングは Virtual DOM を生成するだけという事! 厳密にはもっと複雑なことが行われているかもしれませんが... (詳しい方いたら教えて欲しいです!!)
// このコンポーネントがレンダリングされると、
// Reactは以下のJSX(React要素)を生成
export default function Greeting({ name }) {
return <h1>こんにちは、{name}さん!</h1>;
}
React コンポーネントが再レンダリングされる主な理由は 3 つあります:
状態(state)の変更
コンポーネント内でuseState
やuseReducer
などを使って管理している状態が変更されると、そのコンポーネントは再レンダリングされます。
export default function Counter() {
const [count, setCount] = useState(0);
console.log("再レンダリング");
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
</div>
);
}
上記の例では、ボタンがクリックされるたびにsetCount
が呼び出され、Counter
コンポーネントが再レンダリングされます。
プロパティ(props)の変更
親コンポーネントから渡される props が変更されると、子コンポーネントは再レンダリングされます。
export default function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>親の状態を変更</button>
<Child value={count} />
</div>
);
}
function Child({ value }) {
console.log("子コンポーネントがレンダリングされました");
return <p>子コンポーネントの値: {value}</p>;
}
この例では、Parent
コンポーネントの状態が変更されると、その状態がChild
コンポーネントに props として渡されるため、Child
コンポーネントも再レンダリングされます。
親コンポーネントの再レンダリング
親コンポーネントが再レンダリングされると、デフォルトでは子コンポーネントも再レンダリングされます。これは、props が変更されていなくても発生します。
export default function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<p>カウント: {count}</p>
<button onClick={() => setCount(count + 1)}>増加</button>
<Child />
</div>
);
}
function Child() {
console.log("子コンポーネントがレンダリングされました");
return <p>子コンポーネント</p>;
}
この例では、Child
コンポーネントは props を受け取っていませんが、Parent
が再レンダリングされるたびにChild
も再レンダリングされます。これは React のデフォルトの動作であることを理解しておきましょう!
以下は、不必要な再レンダリングが発生する典型的なパターンです:
export default function Parent() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div>
<button onClick={() => setCount1(count1 + 1)}>Parent Count</button>
<button onClick={() => setCount2(count2 + 1)}>Child Count</button>
<p>Parent: {count1}</p>
<Child count2={count2} />
</div>
);
}
function Child({ count2 }) {
//重い処理
let i = 0;
while (i < 10000000) i++;
return <p>Child: {count2}</p>;
}
上記の例だと、Child
に非常に重い処理が含まれており、Parent Count ボタンを押すたびに Child
コンポーネントが再レンダリングされ、重い処理が走ってしまいますのでその分パフォーマンスの悪化につながってしまいます。
React は、不必要な再レンダリングを防ぐための hook を提供しています:
React.memo
は、コンポーネントを「記憶する」ための機能です。簡単に言うと、「もし渡されるデータ(props)が前回と同じなら、わざわざ再描画しなくてもいいよ」と React に教えるものです。
export default function Parent() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
console.log("親コンポーネントがレンダリングされました");
return (
<div>
<button onClick={() => setCount1(count1 + 1)}>Parent Count</button>
<button onClick={() => setCount2(count2 + 1)}>Child Count</button>
<p>Parent: {count1}</p>
<Child count2={count2} />
</div>
);
}
const Child = React.memo(function Child({ count2 }) {
console.log("子コンポーネントがレンダリングされました");
//重い処理
let i = 0;
while (i < 10000000) i++;
return <p>Child: {count2}</p>;
});
この例では、Parent
のcount1
が変更されても、Child
のcount2
が変更されない限りChild
は再レンダリングされません。
useCallback
は、依存配列が変更されない限り、関数の参照を保持するフックです。これにより、不必要な関数再作成を防ぎ、子コンポーネントへの安定した props を提供できます。
export default function Parent() {
const [count, setCount] = useState(0);
console.log("親コンポーネントがレンダリングされました");
// count が変更されても同じ関数参照が保持される
const handleClick = useCallback(() => {
console.log("クリックされました");
}, []); // 空の依存配列
return (
<div>
<p>Parent: {count}</p>
<button onClick={() => setCount(count + 1)}>Parent Count</button>
<Child onClick={handleClick} />
</div>
);
}
const Child = React.memo(function Child({ onClick }) {
console.log("子コンポーネントがレンダリングされました");
return <button onClick={onClick}>Click</button>;
});
useMemo
は、計算コストの高い値の再計算を防ぐフックです。依存配列が変更されない限り、以前の計算結果を再利用します。
export default function Counter() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(10);
const double = (count) => {
let i = 0;
while (i < 100000000) i++;
return count * 2;
};
const doubleCount = useMemo(() => double(count2), [count2]);
return (
<div>
<p>Counter: {count1}</p>
<button onClick={() => setCount1(count1 + 1)}>Increment count1</button>
<p>
Counter: {count2}, {doubleCount}
</p>
<button onClick={() => setCount2(count2 + 1)}>Increment count2</button>
</div>
);
}
状態の変更が影響する範囲を最小限に抑えるために、コンポーネントを適切に分割し、状態をできるだけローカルに保つことが重要です。
// 良くない例:すべての状態が一つのコンポーネントにある
export default function MonolithicComponent() {
const [user, setUser] = useState({ name: "山田" });
const [count, setCount] = useState(0);
const [products, setProducts] = useState([]);
// userが変更されてもproductsセクションが再レンダリングされる
return (
<div>
<UserProfile user={user} onUpdate={setUser} />
<Counter count={count} onIncrement={() => setCount(count + 1)} />
<ProductList products={products} />
</div>
);
}
// 良い例:状態をコンポーネントに分離する
export default function App() {
return (
<div>
<UserContainer />
<CounterContainer />
<ProductContainer />
</div>
);
}
export default function UserContainer() {
const [user, setUser] = useState({ name: "山田" });
return <UserProfile user={user} onUpdate={setUser} />;
}
export default function CounterContainer() {
const [count, setCount] = useState(0);
return <Counter count={count} onIncrement={() => setCount(count + 1)} />;
}
export default function ProductContainer() {
const [products, setProducts] = useState([]);
return <ProductList products={products} />;
}
React の再レンダリングを理解し、最適化することは、高性能な React アプリケーションを構築するための重要なスキルです。以下が主なポイントです:
再レンダリングの仕組み
状態が変わったとき
props(親から渡されるデータ)が変わったとき
親コンポーネントが更新されたとき
最適化テクニック
React.memo
で不必要な再レンダリングを防止
useCallback
で関数の参照を安定化
useMemo
で計算コストの高い値を記憶
コンポーネントの適切な分割と状態の局所化
最適化は必要なときだけ行いましょう。最初から全部最適化しようとすると、コードが複雑になり、かえって理解しにくくなります。まずはシンプルに作り、本当に遅いと感じたら、ツール(React Developer Tools)を使って問題箇所を特定してから最適化するのがおすすめです!