Reactで開発を進めていると、「なぜかコンポーネントが再レンダリングされない…」という場面に出くわしたことはありませんか?setState
を使って状態を更新したのに画面に変化がない、props
を変更したのに子コンポーネントが動かない、useEffect
がなぜか発火しない……こうした挙動は、React特有のレンダリングの仕組みを理解していないと、なかなか原因にたどり着けません。
この記事では、Reactにおける「再レンダリングがされない原因」と「強制的に再レンダリングする方法」を中心に、現場ですぐに役立つ知識をわかりやすく解説します。また、再レンダリングが発生しないことで発生する落とし穴や、パフォーマンスを意識した最適化テクニックまで幅広くカバー。React開発で「なぜ更新されないのか」に悩んだことがある方は、ぜひ最後までご覧ください。
Reactで再レンダリングされない原因とその対処法
なぜReactコンポーネントは期待通りに更新されないのか?
Reactにおいて、コンポーネントの再レンダリングは特定のルールに従って自動的に発生する仕組みです。開発者が「画面を更新したい」と思った時に、なぜか期待通りにコンポーネントが更新されないという経験は、React開発者なら誰しもが通る道でしょう。

Reactが再レンダリングを決定する基本的なルール
Reactコンポーネントの再レンダリングは、以下の3つのケースで発生します:
- コンポーネント自身のstateが変更された場合
- 親コンポーネントから受け取るpropsが変更された場合
- 親コンポーネントが再レンダリングされた場合
これらのルールを理解する上で重要なのは、Reactが「変更された」と判断する基準です。
シャロー比較(参照の比較)が再レンダリングに与える影響
Reactは、stateやpropsの変更を検知するためにシャロー比較(Shallow Comparison)を使用します。これは、値の内容ではなく参照(メモリアドレス)を比較する仕組みです。
// 例:オブジェクトの参照比較
const obj1 = { name: 'John', age: 30 };
const obj2 = { name: 'John', age: 30 };
console.log(obj1 === obj2); // false(異なる参照)
const obj3 = obj1;
console.log(obj1 === obj3); // true(同じ参照)
この仕組みにより、以下のような問題が発生します:
function UserProfile() {
const [user, setUser] = useState({ name: 'John', age: 30 });
const updateAge = () => {
// ❌ 直接的な変更(ミュータブルな変更)
user.age = 31;
setUser(user); // 参照は変わらないので再レンダリングされない
};
const updateAgeCorrectly = () => {
// ✅ 新しいオブジェクトを作成(イミュータブルな変更)
setUser({ ...user, age: 31 });
};
return (
<div>
<p>Age: {user.age}</p>
<button onClick={updateAge}>間違った更新</button>
<button onClick={updateAgeCorrectly}>正しい更新</button>
</div>
);
}
setStateやpropsを変更しても再レンダリングされない理由
setState
の場合の問題パターン
1. 非同期性とバッチ処理による問題
setState
は非同期で実行され、React 18以降では自動バッチ処理が行われます。これにより、複数のsetState
呼び出しが一度にまとめて処理される場合があります。
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// これらの setState は自動的にバッチ処理される
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
console.log(count); // まだ古い値が表示される
};
// 期待:+3、実際:+1
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>+3 のつもり</button>
</div>
);
}
解決策:
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 関数形式を使用して前の値を基に更新
setCount(prev => prev + 1);
setCount(prev => prev + 1);
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>正しく+3</button>
</div>
);
}
2. 同一値でのスキップ
Reactは、setState
で設定されようとする値が現在の値と同じ場合、再レンダリングをスキップします。
function Example() {
const [value, setValue] = useState('initial');
const handleClick = () => {
setValue('initial'); // 現在の値と同じなので再レンダリングされない
};
console.log('再レンダリングされました'); // 初回のみ表示
return <button onClick={handleClick}>Same Value</button>;
}
props
の場合の問題パターン
オブジェクトや配列の参照が変更されていない場合
最も一般的な問題は、オブジェクトや配列を直接変更してしまうケースです。
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React', completed: false }
]);
const toggleTodo = (id) => {
// ❌ 配列を直接変更
const todo = todos.find(t => t.id === id);
todo.completed = !todo.completed;
setTodos(todos); // 参照は変わらないので再レンダリングされない
};
const toggleTodoCorrectly = (id) => {
// ✅ 新しい配列を作成
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
));
};
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</span>
<button onClick={() => toggleTodo(todo.id)}>
間違った切り替え
</button>
<button onClick={() => toggleTodoCorrectly(todo.id)}>
正しい切り替え
</button>
</li>
))}
</ul>
);
}
不変性(Immutability)の重要性
Reactでは不変性を保つことが極めて重要です。データを変更する際は、既存のオブジェクトを変更するのではなく、新しいオブジェクトを作成する必要があります。
// ❌ ミュータブルな変更
const updateUser = (user) => {
user.name = 'Updated Name';
return user;
};
// ✅ イミュータブルな変更
const updateUser = (user) => {
return { ...user, name: 'Updated Name' };
};
// 深いネストの場合
const updateNestedUser = (user) => {
return {
...user,
profile: {
...user.profile,
preferences: {
...user.profile.preferences,
theme: 'dark'
}
}
};
};
useEffectが実行されないケースと再レンダリングの落とし穴
依存配列の設定ミス
useEffect
は依存配列の値が変更された時に実行されますが、依存配列の設定ミスにより期待通りに動作しないケースがあります。
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
// ❌ 依存配列にuserIdを含めていない
useEffect(() => {
setLoading(true);
fetchUser(userId).then(userData => {
setUser(userData);
setLoading(false);
});
}, []); // userIdが変更されても実行されない
// ✅ 正しい依存配列の設定
useEffect(() => {
setLoading(true);
fetchUser(userId).then(userData => {
setUser(userData);
setLoading(false);
});
}, [userId]); // userIdが変更されると実行される
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
オブジェクトや関数を依存配列に含める際の注意点
オブジェクトや関数を依存配列に含める場合、参照の安定性を確保する必要があります。
function SearchResults({ searchConfig }) {
const [results, setResults] = useState([]);
// ❌ オブジェクトを直接依存配列に含める
useEffect(() => {
searchAPI(searchConfig).then(setResults);
}, [searchConfig]); // searchConfigが毎回新しいオブジェクトの場合、毎回実行される
return (
<ul>
{results.map(item => <li key={item.id}>{item.title}</li>)}
</ul>
);
}
// 親コンポーネントでの問題
function SearchPage() {
const [query, setQuery] = useState('');
return (
<SearchResults
searchConfig={{
query,
limit: 10,
sortBy: 'date'
}} // 毎回新しいオブジェクトが作成される
/>
);
}
解決策:
function SearchPage() {
const [query, setQuery] = useState('');
// useMemoを使用して参照を安定化
const searchConfig = useMemo(() => ({
query,
limit: 10,
sortBy: 'date'
}), [query]);
return <SearchResults searchConfig={searchConfig} />;
}
// または、各プロパティを個別に渡す
function SearchResults({ query, limit, sortBy }) {
const [results, setResults] = useState([]);
useEffect(() => {
searchAPI({ query, limit, sortBy }).then(setResults);
}, [query, limit, sortBy]);
return (
<ul>
{results.map(item => <li key={item.id}>{item.title}</li>)}
</ul>
);
}
無限ループの原因と回避策
useEffect
による無限ループは、依存配列の設定ミスや、エフェクト内でstateを更新することで発生します。
function ProblematicComponent() {
const [count, setCount] = useState(0);
const [data, setData] = useState([]);
// ❌ 無限ループの例
useEffect(() => {
setData([...data, count]); // dataを更新
}, [data, count]); // dataが依存配列に含まれているので無限ループ
// ✅ 修正版1: 関数型アップデート
useEffect(() => {
setData(prev => [...prev, count]);
}, [count]); // dataを依存配列から除外
// ✅ 修正版2: useCallbackで関数を安定化
const addToData = useCallback((newValue) => {
setData(prev => [...prev, newValue]);
}, []);
useEffect(() => {
addToData(count);
}, [count, addToData]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
<ul>
{data.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}
useEffectのクリーンアップ関数の重要性
非同期処理やイベントリスナーを使用する場合、クリーンアップ関数を適切に設定することで、メモリリークや意図しない動作を防げます。
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// クリーンアップ関数
return () => clearInterval(interval);
}, []);
return <div>Timer: {seconds}s</div>;
}
function DataFetcher({ userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
fetchUserData(userId)
.then(result => {
if (!cancelled) {
setData(result);
setLoading(false);
}
})
.catch(error => {
if (!cancelled) {
console.error('Error fetching data:', error);
setLoading(false);
}
});
return () => {
cancelled = true; // コンポーネントがアンマウントされた場合のクリーンアップ
};
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{data?.name}</div>;
}
再レンダリングされない問題を解決するためには、Reactの基本的な仕組みを理解し、不変性を保ち、適切な依存配列を設定することが重要です。次のセクションでは、これらの問題を踏まえて、どのように強制的に再レンダリングを行うかについて詳しく解説します。
強制的にReactコンポーネントを再レンダリングする方法
前のセクションで解説したような「期待通りに再レンダリングされない」問題を解決するための根本的なアプローチが推奨されますが、時には強制的に再レンダリングを実行したい場面があります。このセクションでは、そのような状況での対処法を、推奨度の高い順に解説します。

クラスコンポーネントでのforceUpdate()の使い方と注意点
forceUpdate()
の基本的な機能
クラスコンポーネントでは、forceUpdate()
メソッドを使用して強制的に再レンダリングを実行できます。このメソッドは、shouldComponentUpdate()
の結果に関係なく、コンポーネントの再レンダリングを強制します。
class ForceUpdateExample extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.externalData = { value: 'initial' };
}
handleForceUpdate = () => {
// 外部データを変更(stateやpropsではない)
this.externalData.value = `Updated at ${new Date().toLocaleTimeString()}`;
// 強制的に再レンダリング
this.forceUpdate();
};
render() {
console.log('Component re-rendered');
return (
<div>
<p>External Data: {this.externalData.value}</p>
<p>State Count: {this.state.count}</p>
<button onClick={this.handleForceUpdate}>
Force Update
</button>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Normal Update
</button>
</div>
);
}
}
forceUpdate()
を使用すべき状況
forceUpdate()
は基本的に最後の手段として使用すべきです。適切な使用例は以下の通りです:
- 外部ライブラリとの統合で、Reactの通常の更新サイクルの外で管理されるデータを扱う場合
- デバッグ目的で、特定のタイミングでの再レンダリングを確認したい場合
- レガシーコードの移行過程での一時的な対処として
class ThirdPartyLibraryIntegration extends React.Component {
componentDidMount() {
// 外部ライブラリの初期化
this.externalLib = new SomeExternalLibrary();
// 外部ライブラリのイベントリスナー
this.externalLib.on('dataChanged', () => {
// 外部ライブラリのデータが変更された時に強制更新
this.forceUpdate();
});
}
componentWillUnmount() {
// クリーンアップ
if (this.externalLib) {
this.externalLib.removeAllListeners();
}
}
render() {
const data = this.externalLib ? this.externalLib.getData() : null;
return (
<div>
<h3>External Library Data:</h3>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
}
forceUpdate()
使用時の注意点とリスク

1. パフォーマンスへの影響
forceUpdate()
は最適化を無視してレンダリングを強制するため、パフォーマンスに悪影響を与える可能性があります。
class PerformanceProblematic extends React.Component {
handleMultipleForceUpdates = () => {
// ❌ 複数の forceUpdate() は避ける
this.forceUpdate();
this.forceUpdate();
this.forceUpdate();
};
render() {
// 重い処理の例
const heavyCalculation = Array.from({ length: 10000 }, (_, i) => i * i);
return (
<div>
<button onClick={this.handleMultipleForceUpdates}>
複数の Force Update(避けるべき)
</button>
<p>Heavy calculation result: {heavyCalculation.length}</p>
</div>
);
}
}
2. デバッグの困難さ
forceUpdate()
を多用すると、コンポーネントの更新タイミングが予測しにくくなり、デバッグが困難になります。
class DebuggingDifficult extends React.Component {
componentDidUpdate(prevProps, prevState) {
// forceUpdate()による更新では、prevPropsとprevStateは変更されない
console.log('Updated, but why?', {
prevProps,
currentProps: this.props,
prevState,
currentState: this.state
});
}
render() {
return <div>Debug me!</div>;
}
}
関数コンポーネントでの再レンダリング(useReducer, key再生成)
関数コンポーネントにはforceUpdate()
がないため、代替手段を使用して強制的な再レンダリングを実現します。
useReducer
を用いた強制再レンダリング
useReducer
は状態管理だけでなく、強制的な再レンダリングのトリガーとしても使用できます。
import React, { useReducer, useRef } from 'react';
function ForceUpdateWithReducer() {
// 強制更新用のreducer
const [, forceUpdate] = useReducer(x => x + 1, 0);
// 外部データの参照
const externalDataRef = useRef({ value: 'initial', timestamp: Date.now() });
const handleForceUpdate = () => {
// 外部データを更新
externalDataRef.current = {
value: `Updated at ${new Date().toLocaleTimeString()}`,
timestamp: Date.now()
};
// 強制的に再レンダリング
forceUpdate();
};
const handleNormalUpdate = () => {
// 通常の状態更新
externalDataRef.current = {
...externalDataRef.current,
value: `Normal update at ${new Date().toLocaleTimeString()}`
};
forceUpdate();
};
return (
<div>
<h3>useReducer Force Update Example</h3>
<p>External Data: {externalDataRef.current.value}</p>
<p>Timestamp: {externalDataRef.current.timestamp}</p>
<button onClick={handleForceUpdate}>
Force Update
</button>
<button onClick={handleNormalUpdate}>
Normal Update
</button>
</div>
);
}
より実用的なuseReducer
の使用例
import React, { useReducer, useRef, useEffect } from 'react';
// カスタムフックとして再利用可能にする
function useForceUpdate() {
const [, forceUpdate] = useReducer(x => x + 1, 0);
return forceUpdate;
}
function UIRefreshExample() {
const forceUpdate = useForceUpdate();
const dataRef = useRef([]);
const [autoRefresh, setAutoRefresh] = React.useState(false);
// 外部データを追加する関数
const addDataItem = () => {
dataRef.current.push({
id: Date.now(),
value: Math.random().toFixed(2),
timestamp: new Date().toLocaleTimeString()
});
// データを追加後、UIを更新
forceUpdate();
};
// 自動リフレッシュ機能
useEffect(() => {
if (!autoRefresh) return;
const interval = setInterval(() => {
addDataItem();
}, 2000);
return () => clearInterval(interval);
}, [autoRefresh]);
const clearData = () => {
dataRef.current = [];
forceUpdate();
};
return (
<div>
<h3>UI Refresh Example</h3>
<div>
<button onClick={addDataItem}>Add Data</button>
<button onClick={clearData}>Clear Data</button>
<button onClick={() => setAutoRefresh(!autoRefresh)}>
{autoRefresh ? 'Stop' : 'Start'} Auto Refresh
</button>
</div>
<div>
<h4>Data Items ({dataRef.current.length}):</h4>
<ul>
{dataRef.current.map(item => (
<li key={item.id}>
{item.value} - {item.timestamp}
</li>
))}
</ul>
</div>
</div>
);
}
key
属性の再生成による再レンダリング
key
属性を変更することで、コンポーネント全体を再マウントさせることができます。これは再レンダリングというより、コンポーネントの完全な再構築です。
import React, { useState } from 'react';
function FormWithReset() {
const [formKey, setFormKey] = useState(0);
const [submitCount, setSubmitCount] = useState(0);
const resetForm = () => {
// keyを変更してフォームを完全にリセット
setFormKey(prev => prev + 1);
};
return (
<div>
<h3>Form Reset Example</h3>
<p>Form has been reset {formKey} times</p>
<p>Submitted {submitCount} times</p>
{/* keyを変更することでFormComponentを完全に再マウント */}
<FormComponent
key={formKey}
onSubmit={() => setSubmitCount(prev => prev + 1)}
/>
<button onClick={resetForm}>
Reset Form (Key Change)
</button>
</div>
);
}
function FormComponent({ onSubmit }) {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
console.log('FormComponent mounted/re-mounted');
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', { name, email, message });
onSubmit();
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
</div>
<div>
<label>
Email:
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</label>
</div>
<div>
<label>
Message:
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</label>
</div>
<button type="submit">Submit</button>
</form>
);
}
key変更による再レンダリングの原理と正しい実装例
Reactのkey
属性の役割と仮想DOM
key
属性は、Reactがコンポーネントの同一性を判断するために使用する重要な属性です。仮想DOMの差分更新(Reconciliation)プロセスにおいて、key
は以下の役割を果たします:
- リストのアイテムの識別: 配列内の要素を一意に識別する
- コンポーネントの同一性判断: 同じコンポーネントの再利用か、新しいコンポーネントの作成かを決定する
- 最適化の基準: 不要なDOM操作を避けるための判断材料
key
変更が再レンダリングを引き起こすメカニズム
import React, { useState, useEffect } from 'react';
function KeyChangeExample() {
const [componentKey, setComponentKey] = useState('initial');
const [counter, setCounter] = useState(0);
return (
<div>
<h3>Key Change Mechanism</h3>
<p>Current key: {componentKey}</p>
<p>Counter: {counter}</p>
{/* keyが変更されるとExpensiveComponentは完全に再マウントされる */}
<ExpensiveComponent
key={componentKey}
initialValue={counter}
/>
<div>
<button onClick={() => setCounter(counter + 1)}>
Increment Counter
</button>
<button onClick={() => setComponentKey(`key-${Date.now()}`)}>
Change Key (Remount Component)
</button>
<button onClick={() => setComponentKey('reset')}>
Reset Key
</button>
</div>
</div>
);
}
function ExpensiveComponent({ initialValue }) {
const [internalState, setInternalState] = useState(initialValue);
const [mountTime] = useState(new Date().toLocaleTimeString());
useEffect(() => {
console.log('ExpensiveComponent mounted at:', mountTime);
// 重い初期化処理のシミュレーション
const timer = setTimeout(() => {
console.log('Expensive initialization completed');
}, 1000);
return () => {
console.log('ExpensiveComponent unmounted');
clearTimeout(timer);
};
}, [mountTime]);
return (
<div style={{
border: '2px solid #007bff',
padding: '20px',
margin: '10px 0'
}}>
<h4>Expensive Component</h4>
<p>Mounted at: {mountTime}</p>
<p>Initial value: {initialValue}</p>
<p>Internal state: {internalState}</p>
<button onClick={() => setInternalState(internalState + 1)}>
Update Internal State
</button>
</div>
);
}
実用的なkey
変更のユースケース
1. フォームのリセット機能
function UserRegistrationForm() {
const [formVersion, setFormVersion] = useState(1);
const [savedForms, setSavedForms] = useState([]);
const handleFormSubmit = (formData) => {
setSavedForms(prev => [...prev, { ...formData, id: Date.now() }]);
// フォーム送信後、新しいフォームを作成
setFormVersion(prev => prev + 1);
};
const createNewForm = () => {
setFormVersion(prev => prev + 1);
};
return (
<div>
<h3>User Registration Forms</h3>
<p>Form Version: {formVersion}</p>
<RegistrationForm
key={formVersion}
onSubmit={handleFormSubmit}
/>
<button onClick={createNewForm}>
Create New Form
</button>
<div>
<h4>Saved Forms ({savedForms.length}):</h4>
<ul>
{savedForms.map(form => (
<li key={form.id}>
{form.name} - {form.email}
</li>
))}
</ul>
</div>
</div>
);
}
function RegistrationForm({ onSubmit }) {
const [formData, setFormData] = useState({
name: '',
email: '',
phone: ''
});
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(formData);
};
const handleInputChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="text"
placeholder="Name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
/>
</div>
<div>
<input
type="email"
placeholder="Email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
/>
</div>
<div>
<input
type="tel"
placeholder="Phone"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
/>
</div>
<button type="submit">Register</button>
</form>
);
}
2. アニメーションの再トリガー
import React, { useState, useEffect } from 'react';
function AnimationRetrigger() {
const [animationKey, setAnimationKey] = useState(0);
const [isAutoPlay, setIsAutoPlay] = useState(false);
const restartAnimation = () => {
setAnimationKey(prev => prev + 1);
};
useEffect(() => {
if (!isAutoPlay) return;
const interval = setInterval(() => {
restartAnimation();
}, 3000);
return () => clearInterval(interval);
}, [isAutoPlay]);
return (
<div>
<h3>Animation Retrigger Example</h3>
<AnimatedComponent key={animationKey} />
<div>
<button onClick={restartAnimation}>
Restart Animation
</button>
<button onClick={() => setIsAutoPlay(!isAutoPlay)}>
{isAutoPlay ? 'Stop' : 'Start'} Auto Play
</button>
</div>
</div>
);
}
function AnimatedComponent() {
const [phase, setPhase] = useState('initial');
useEffect(() => {
const sequence = [
{ phase: 'fadeIn', delay: 0 },
{ phase: 'slideIn', delay: 500 },
{ phase: 'scaleUp', delay: 1000 },
{ phase: 'complete', delay: 1500 }
];
const timers = sequence.map(({ phase, delay }) =>
setTimeout(() => setPhase(phase), delay)
);
return () => timers.forEach(timer => clearTimeout(timer));
}, []);
const getStyle = () => {
const baseStyle = {
width: '200px',
height: '100px',
backgroundColor: '#007bff',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '20px 0',
transition: 'all 0.5s ease'
};
switch (phase) {
case 'initial':
return { ...baseStyle, opacity: 0, transform: 'translateX(-100px)' };
case 'fadeIn':
return { ...baseStyle, opacity: 0.5, transform: 'translateX(-100px)' };
case 'slideIn':
return { ...baseStyle, opacity: 1, transform: 'translateX(0)' };
case 'scaleUp':
return { ...baseStyle, opacity: 1, transform: 'translateX(0) scale(1.1)' };
case 'complete':
return { ...baseStyle, opacity: 1, transform: 'translateX(0) scale(1)' };
default:
return baseStyle;
}
};
return (
<div style={getStyle()}>
Animation Phase: {phase}
</div>
);
}
安易なkey
変更の危険性
key
変更は強力な機能ですが、不適切な使用は以下の問題を引き起こします:
1. パフォーマンスの低下
// ❌ 毎回新しいkeyを生成(避けるべき)
function BadKeyExample() {
const [items, setItems] = useState([1, 2, 3]);
return (
<ul>
{items.map(item => (
<li key={Math.random()}> {/* 毎回異なるkey */}
{item}
</li>
))}
</ul>
);
}
// ✅ 安定したkeyを使用
function GoodKeyExample() {
const [items, setItems] = useState([
{ id: 1, value: 'Item 1' },
{ id: 2, value: 'Item 2' },
{ id: 3, value: 'Item 3' }
]);
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.value}
</li>
))}
</ul>
);
}
2. 状態の予期しない喪失
function StateLossExample() {
const [resetKey, setResetKey] = useState(0);
const [globalCounter, setGlobalCounter] = useState(0);
return (
<div>
<p>Global Counter: {globalCounter}</p>
<button onClick={() => setGlobalCounter(prev => prev + 1)}>
Increment Global
</button>
{/* keyを変更すると、StatefulComponentの内部状態が失われる */}
<StatefulComponent
key={resetKey}
globalCounter={globalCounter}
/>
<button onClick={() => setResetKey(prev => prev + 1)}>
Reset Component (状態が失われる)
</button>
</div>
);
}
function StatefulComponent({ globalCounter }) {
const [localCounter, setLocalCounter] = useState(0);
return (
<div style={{ border: '1px solid #ccc', padding: '10px', margin: '10px 0' }}>
<p>Local Counter: {localCounter}</p>
<p>Global Counter: {globalCounter}</p>
<button onClick={() => setLocalCounter(prev => prev + 1)}>
Increment Local
</button>
</div>
);
}
強制的な再レンダリングは、適切に使用すれば強力なツールですが、基本的には最後の手段として位置づけるべきです。まずは、Reactの標準的なstate管理とpropsの仕組みを使って問題を解決できないかを検討し、それでも必要な場合にのみ、これらの手法を使用することを推奨します。
再レンダリングの最適化とパフォーマンス向上テクニック
Reactアプリケーションのパフォーマンスを向上させるには、無駄な再レンダリングを防ぐことが重要です。この章では、実際の開発現場で使える最適化テクニックを詳しく解説します。
不要な再レンダリングを防ぐ最適化手法【React.memo・useCallback・useMemo】
React.memo:propsの変更がない場合の再レンダリングスキップ
React.memo
は、コンポーネントをメモ化し、propsに変更がない場合に再レンダリングをスキップする高階コンポーネントです。内部的には浅い比較(shallow comparison)を行い、propsの値が前回と同じであれば、コンポーネントの再実行を避けます。
// 通常のコンポーネント(毎回再レンダリング)
const UserProfile = ({ user, onClick }) => {
console.log('UserProfile rendered'); // 親の再レンダリング時に毎回実行される
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={onClick}>詳細を見る</button>
</div>
);
};
// React.memoで最適化されたコンポーネント
const OptimizedUserProfile = React.memo(({ user, onClick }) => {
console.log('OptimizedUserProfile rendered'); // propsが変更された時のみ実行
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={onClick}>詳細を見る</button>
</div>
);
});
ただし、React.memo
を使う際は以下の点に注意が必要です:
使うべきケース:
- 重い計算やレンダリング処理を含むコンポーネント
- 頻繁に親コンポーネントが再レンダリングされるが、propsが変わらないことが多い場合
- リストアイテムなど、多数のコンポーネントが表示される場合
避けるべきケース:
- propsが毎回変わるコンポーネント(メモ化の効果がない)
- 軽量なコンポーネント(メモ化のオーバーヘッドの方が大きい)
- 開発初期段階(premature optimization)
useCallback:関数の再生成を防ぐ
useCallback
は、関数をメモ化し、依存配列の値が変更されない限り同じ関数インスタンスを返します。これにより、子コンポーネントへのpropsとして渡す関数の不要な再生成を防げます。
const UserList = () => {
const [users, setUsers] = useState([]);
const [filter, setFilter] = useState('');
// 悪い例:毎回新しい関数が生成される
const handleUserClick = (userId) => {
console.log(`User ${userId} clicked`);
// 何らかの処理
};
// 良い例:useCallbackでメモ化
const handleUserClickOptimized = useCallback((userId) => {
console.log(`User ${userId} clicked`);
// 何らかの処理
}, []); // 依存配列が空なので、コンポーネントのライフサイクル中は同じ関数
// 依存配列がある場合の例
const handleUserClickWithDeps = useCallback((userId) => {
console.log(`User ${userId} clicked with filter: ${filter}`);
// filterを使用する処理
}, [filter]); // filterが変更された時のみ新しい関数を生成
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="ユーザーを検索"
/>
{users.map(user => (
<OptimizedUserProfile
key={user.id}
user={user}
onClick={() => handleUserClickOptimized(user.id)}
/>
))}
</div>
);
};
useCallback
は特にuseEffect
の依存配列でも威力を発揮します:
const DataFetcher = ({ userId }) => {
const [data, setData] = useState(null);
// useCallbackなしの場合、毎回新しい関数が生成され、useEffectが実行される
const fetchData = useCallback(async () => {
const response = await fetch(`/api/users/${userId}`);
const result = await response.json();
setData(result);
}, [userId]); // userIdが変更された時のみ新しい関数を生成
useEffect(() => {
fetchData();
}, [fetchData]); // fetchDataが変更された時のみ実行
return <div>{data ? data.name : 'Loading...'}</div>;
};
useMemo:計算結果のキャッシュ
useMemo
は、計算結果をメモ化し、依存配列の値が変更されない限り同じ結果を返します。特に重い計算処理やオブジェクトの生成を伴う場合に効果的です。
const ExpensiveComponent = ({ items, filter }) => {
// 悪い例:毎回重い計算が実行される
const expensiveCalculation = items
.filter(item => item.category === filter)
.map(item => ({
...item,
processedValue: complexCalculation(item.value)
}))
.sort((a, b) => b.processedValue - a.processedValue);
// 良い例:useMemoで計算結果をキャッシュ
const expensiveCalculationMemo = useMemo(() => {
console.log('Heavy calculation executed'); // 依存配列が変更された時のみ実行
return items
.filter(item => item.category === filter)
.map(item => ({
...item,
processedValue: complexCalculation(item.value)
}))
.sort((a, b) => b.processedValue - a.processedValue);
}, [items, filter]); // itemsかfilterが変更された時のみ再計算
// オブジェクトの生成もメモ化できる
const chartConfig = useMemo(() => ({
type: 'bar',
data: expensiveCalculationMemo,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${filter}のデータ`
}
}
}
}), [expensiveCalculationMemo, filter]);
return (
<div>
<Chart config={chartConfig} />
<ItemList items={expensiveCalculationMemo} />
</div>
);
};
function complexCalculation(value) {
// 重い計算処理をシミュレート
let result = value;
for (let i = 0; i < 1000000; i++) {
result = Math.sqrt(result + i);
}
return result;
}
3つのフックの連携による最適化
これらのフックを組み合わせることで、より効果的な最適化が可能です:
const OptimizedDashboard = React.memo(({ userId, settings }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
// API呼び出し関数をメモ化
const fetchUserData = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}/dashboard`);
const result = await response.json();
setData(result);
} finally {
setLoading(false);
}
}, [userId]);
// 重い計算処理をメモ化
const processedData = useMemo(() => {
if (!data) return null;
return data.map(item => ({
...item,
calculatedValue: item.value * settings.multiplier,
formatted: formatCurrency(item.value * settings.multiplier)
}));
}, [data, settings.multiplier]);
// イベントハンドラーをメモ化
const handleRefresh = useCallback(() => {
fetchUserData();
}, [fetchUserData]);
useEffect(() => {
fetchUserData();
}, [fetchUserData]);
if (loading) return <div>Loading...</div>;
return (
<div>
<button onClick={handleRefresh}>データを更新</button>
<DataTable data={processedData} />
</div>
);
});
再レンダリングのタイミングと仕組み:仮想DOMと差分更新の全て
仮想DOMの役割と仕組み
仮想DOM(Virtual DOM)は、実際のDOM要素のJavaScriptオブジェクト表現です。Reactは仮想DOMを使用して、実際のDOM操作を最小限に抑え、パフォーマンスを向上させます。
// 実際のDOM操作(重い処理)
const realDOMElement = document.createElement('div');
realDOMElement.className = 'user-card';
realDOMElement.innerHTML = '<h3>John Doe</h3><p>john@example.com</p>';
document.body.appendChild(realDOMElement);
// 仮想DOMの表現(軽い処理)
const virtualDOMElement = {
type: 'div',
props: {
className: 'user-card',
children: [
{ type: 'h3', props: { children: 'John Doe' } },
{ type: 'p', props: { children: 'john@example.com' } }
]
}
};
仮想DOMの処理フローは以下の通りです:
- 状態変更の検知:stateやpropsが変更される
- 新しい仮想DOMツリーの生成:更新後のUIを表現する仮想DOMを作成
- 差分計算(Diffing):前回の仮想DOMと比較し、変更箇所を特定
- 実際のDOM更新(Reconciliation):必要最小限のDOM操作を実行
差分更新(Reconciliation)の詳細
Reactの差分更新アルゴリズムは、効率的にUIを更新するための核心的な仕組みです:
// 更新前の仮想DOM
const beforeVDOM = {
type: 'ul',
props: {
children: [
{ type: 'li', key: '1', props: { children: 'Item 1' } },
{ type: 'li', key: '2', props: { children: 'Item 2' } },
{ type: 'li', key: '3', props: { children: 'Item 3' } }
]
}
};
// 更新後の仮想DOM(Item 2が削除され、Item 4が追加)
const afterVDOM = {
type: 'ul',
props: {
children: [
{ type: 'li', key: '1', props: { children: 'Item 1' } },
{ type: 'li', key: '3', props: { children: 'Item 3' } },
{ type: 'li', key: '4', props: { children: 'Item 4' } }
]
}
};
// Reactの差分アルゴリズムが実行する処理:
// 1. key='1'の要素:変更なし(スキップ)
// 2. key='2'の要素:削除対象として認識
// 3. key='3'の要素:位置変更として認識
// 4. key='4'の要素:新規追加として認識
差分更新の最適化ルール:
- 要素の型が異なる場合:古い要素を破棄し、新しい要素を作成
- 同じ型の要素の場合:属性の差分のみを更新
- key属性の活用:リスト内の要素の同一性を効率的に判断
再レンダリングのライフサイクル
React 18以降の再レンダリングは、以下の2つのフェーズで実行されます:
const ExampleComponent = ({ count }) => {
const [internalState, setInternalState] = useState(0);
// Renderフェーズ:新しい仮想DOMの生成
console.log('Render phase: Creating virtual DOM');
useEffect(() => {
// Commitフェーズ:実際のDOM更新後に実行
console.log('Commit phase: DOM has been updated');
}, [count]);
useLayoutEffect(() => {
// Commitフェーズ:DOM更新後、描画前に同期実行
console.log('Commit phase: Before browser painting');
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Internal: {internalState}</p>
<button onClick={() => setInternalState(prev => prev + 1)}>
Increment Internal
</button>
</div>
);
};
Renderフェーズ:
- 新しい仮想DOMツリーの生成
- コンポーネント関数の実行
- hooksの実行
- 差分計算の準備
Commitフェーズ:
- 実際のDOM更新
useEffect
の実行- refs の更新
- ライフサイクルメソッドの実行
デバッグ効率が劇的に向上!再レンダリングされないときの効果的な特定手順
React Developer Toolsを使った再レンダリングの可視化
React Developer Toolsは、再レンダリングの問題を特定する最も強力なツールです:
1. Profilerを使った パフォーマンス分析
// 分析対象のコンポーネント
const ProblematicComponent = ({ data }) => {
const [filter, setFilter] = useState('');
// 問題:毎回新しいオブジェクトが生成される
const processedData = data.map(item => ({
...item,
processed: true
}));
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
<DataList data={processedData} filter={filter} />
</div>
);
};
// Profilerでの確認ポイント:
// - コンポーネントの再レンダリング頻度
// - 各再レンダリングの実行時間
// - 再レンダリングの理由(props change, state change, parent re-render)
2. Componentsタブでの状態確認
// デバッグのためのカスタムフック
const useDebugValue = (value, label) => {
const ref = useRef(value);
useEffect(() => {
if (ref.current !== value) {
console.log(`${label} changed:`, {
from: ref.current,
to: value
});
ref.current = value;
}
});
// React Developer Toolsで確認可能
React.useDebugValue(value, (val) => `${label}: ${JSON.stringify(val)}`);
return value;
};
const DebuggableComponent = ({ user, settings }) => {
const [count, setCount] = useState(0);
// Developer Toolsで値の変化を追跡
useDebugValue(user, 'User');
useDebugValue(settings, 'Settings');
useDebugValue(count, 'Count');
return (
<div>
<p>User: {user.name}</p>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>
Increment
</button>
</div>
);
};
console.logを使った詳細な追跡
// state や props の変化を追跡するカスタムフック
const useTraceUpdate = (props) => {
const prev = useRef(props);
useEffect(() => {
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
if (prev.current[k] !== v) {
ps[k] = [prev.current[k], v];
}
return ps;
}, {});
if (Object.keys(changedProps).length > 0) {
console.log('Changed props:', changedProps);
}
prev.current = props;
});
};
// 使用例
const TrackedComponent = (props) => {
useTraceUpdate(props);
return <div>{props.children}</div>;
};
カスタムフックを使った高度なデバッグ
// 再レンダリングの原因を特定するカスタムフック
const useWhyDidYouUpdate = (name, props) => {
const previousProps = useRef();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changes = {};
allKeys.forEach(key => {
if (previousProps.current[key] !== props[key]) {
changes[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object.keys(changes).length) {
console.log('[why-did-you-update]', name, changes);
}
}
previousProps.current = props;
});
};
// React.memo が期待通りに動作しない場合のデバッグ
const MemoizedComponent = React.memo((props) => {
useWhyDidYouUpdate('MemoizedComponent', props);
return <div>{props.children}</div>;
});
// useCallback/useMemo の依存配列をデバッグ
const useDebugDependencies = (name, dependencies) => {
const prevDeps = useRef(dependencies);
useEffect(() => {
const changes = dependencies.map((dep, index) => {
const prev = prevDeps.current[index];
if (prev !== dep) {
return {
index,
from: prev,
to: dep,
changed: true
};
}
return { index, value: dep, changed: false };
});
const hasChanges = changes.some(change => change.changed);
if (hasChanges) {
console.log(`[${name}] Dependencies changed:`, changes);
}
prevDeps.current = dependencies;
});
};
// 使用例
const OptimizedComponent = ({ data, filter }) => {
const processedData = useMemo(() => {
useDebugDependencies('processedData useMemo', [data, filter]);
return data.filter(item => item.category === filter);
}, [data, filter]);
const handleClick = useCallback(() => {
useDebugDependencies('handleClick useCallback', [data]);
console.log('Clicked with data:', data);
}, [data]);
return (
<div>
<button onClick={handleClick}>Process</button>
<DataDisplay data={processedData} />
</div>
);
};
実践的なデバッグワークフロー
Step 1: 問題の特定
// 1. 再レンダリング回数を計測
const RenderCounter = ({ name, children }) => {
const renderCount = useRef(0);
renderCount.current++;
console.log(`${name} rendered ${renderCount.current} times`);
return children;
};
// 使用例
<RenderCounter name="UserList">
<UserList users={users} />
</RenderCounter>
Step 2: 原因の深堀り
// 2. 再レンダリングの原因を特定
const useRenderLogger = (componentName, props) => {
const renderCount = useRef(0);
const prevProps = useRef(props);
renderCount.current++;
console.group(`🔄 ${componentName} render #${renderCount.current}`);
if (prevProps.current) {
const propsKeys = Object.keys(props);
const changedProps = propsKeys.filter(
key => prevProps.current[key] !== props[key]
);
if (changedProps.length > 0) {
console.log('Changed props:', changedProps);
changedProps.forEach(key => {
console.log(` ${key}:`, {
from: prevProps.current[key],
to: props[key]
});
});
} else {
console.log('No props changed (parent re-render)');
}
}
console.groupEnd();
prevProps.current = props;
};
Step 3: 解決策の実装と検証
// 3. 最適化後の効果測定
const usePerformanceMonitor = (name) => {
const startTime = useRef(performance.now());
useEffect(() => {
const endTime = performance.now();
const renderTime = endTime - startTime.current;
console.log(`⚡ ${name} render time: ${renderTime.toFixed(2)}ms`);
startTime.current = performance.now();
});
};
// 最適化されたコンポーネント
const OptimizedUserList = React.memo(({ users, onUserClick }) => {
usePerformanceMonitor('OptimizedUserList');
useRenderLogger('OptimizedUserList', { users, onUserClick });
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onClick={onUserClick}
/>
))}
</div>
);
});
これらのデバッグツールとテクニックを組み合わせることで、再レンダリングの問題を効率的に特定し、適切な最適化を実装できます。
よくある質問(FAQ)
React開発において、再レンダリングに関する疑問や悩みは開発者にとって非常に一般的です。ここでは、実際の開発現場でよく遭遇する質問とその回答を整理しました。
-
useStateのセッター関数を複数回呼ぶとどうなりますか?
-
React 18以降では、複数の
setState
呼び出しは自動的にバッチ処理されるため、1回の再レンダリングで処理されます。const [count, setCount] = useState(0); const [name, setName] = useState(''); const handleClick = () => { setCount(count + 1); // バッチ処理される setName('新しい名前'); // バッチ処理される // 結果:1回だけ再レンダリングが発生 };
ただし、同じstateに対して複数回セッターを呼ぶ場合は、関数型更新を使用することで確実に前の値を参照できます:
const handleClick = () => { setCount(prev => prev + 1); setCount(prev => prev + 1); // 確実に+2される };
-
Contextが変更された場合、関連コンポーネントはすべて再レンダリングされますか?
-
はい、Contextの値が変更されると、そのContextを購読しているすべてのコンポーネントが再レンダリングされます。これは、深いコンポーネントツリーでパフォーマンスの問題を引き起こす可能性があります。
// 問題のあるContext使用例 const AppContext = createContext(); const App = () => { const [user, setUser] = useState(null); const [theme, setTheme] = useState('light'); // userまたはthemeが変更されると、全てのコンシューマーが再レンダリング const value = { user, theme, setUser, setTheme }; return ( <AppContext.Provider value={value}> <ComponentTree /> </AppContext.Provider> ); };
解決策:
- Contextを用途別に分割する
useMemo
で値をメモ化する- 必要に応じてContextの粒度を細かくする
-
shouldComponentUpdate(クラスコンポーネント)とReact.memo(関数コンポーネント)の違いは何ですか?
-
A: 両者は同じ目的(不要な再レンダリングの防止)を持ちますが、使用方法と動作に違いがあります。
shouldComponentUpdate(クラスコンポーネント):
- メソッドとしてコンポーネント内に定義
true
を返すと再レンダリング、false
を返すとスキップ- 手動で比較ロジックを実装する必要がある
class MyComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return nextProps.value !== this.props.value; } render() { return <div>{this.props.value}</div>; } }
React.memo(関数コンポーネント):
- 高階コンポーネントとしてコンポーネントをラップ
- デフォルトでシャロー比較を実行
- 必要に応じてカスタム比較関数を提供可能
const MyComponent = React.memo(({ value }) => { return <div>{value}</div>; }); // カスタム比較関数を使用する場合 const MyComponent = React.memo(({ value }) => { return <div>{value}</div>; }, (prevProps, nextProps) => { return prevProps.value === nextProps.value; });
-
大規模アプリケーションでの再レンダリング戦略はどのように考えればよいですか?
-
大規模アプリケーションでは、以下の戦略を段階的に適用することが重要です:
1. 状態管理の最適化
- グローバル状態とローカル状態の適切な分離
- 必要に応じてReduxやZustandなどの状態管理ライブラリの活用
- Contextの用途別分割
2. コンポーネント設計の見直し
- 責任の分離(プレゼンテーション層とビジネスロジック層)
- 適切なコンポーネントの粒度設定
- props drilling の回避
3. パフォーマンス最適化の適用
- 重要な境界でのReact.memoの活用
- 重い計算処理でのuseMemoの使用
- イベントハンドラーでのuseCallbackの使用
4. 監視とデバッグ
- React Developer Toolsでの定期的なプロファイリング
- パフォーマンスメトリクスの継続的な監視
- ボトルネックの早期発見と対応
-
useEffectの依存配列に関数を含める場合の注意点は何ですか?
-
関数を依存配列に含める場合、その関数が再レンダリングのたびに再生成されると、
useEffect
が不要に実行される可能性があります。問題のある例:
const MyComponent = ({ userId }) => { const fetchUserData = () => { // API呼び出し }; useEffect(() => { fetchUserData(); }, [fetchUserData]); // 毎回再生成される関数が依存配列に含まれる return <div>...</div>; };
解決策:
const MyComponent = ({ userId }) => { const fetchUserData = useCallback(() => { // API呼び出し }, [userId]); // userIdが変更された場合のみ再生成 useEffect(() => { fetchUserData(); }, [fetchUserData]); return <div>...</div>; };
または、関数を
useEffect
内に定義する:const MyComponent = ({ userId }) => { useEffect(() => { const fetchUserData = () => { // API呼び出し }; fetchUserData(); }, [userId]); // 必要な値のみを依存配列に含める return <div>...</div>; };
まとめ
React開発において、再レンダリングの挙動を正しく理解することは、効率的で保守性の高いアプリケーション開発の基盤となります。本記事では、再レンダリングが期待通りに動作しない原因から、強制的に再レンダリングを実行する方法、そして最適化テクニックまでを網羅的に解説してきました。
重要ポイント
再レンダリングが起こらない主な原因
- stateやpropsの参照が変更されていない(シャロー比較の影響)
- 配列やオブジェクトを直接変更している(不変性の原則違反)
- useEffectの依存配列が適切に設定されていない
強制再レンダリングの実装方法
- クラスコンポーネント:
forceUpdate()
(非推奨だが理解は必要) - 関数コンポーネント:
useReducer
による状態更新トリガー key
属性の変更によるコンポーネントの再マウント
パフォーマンス最適化の三大手法
React.memo
:props変更時のみ再レンダリングuseCallback
:関数の再生成を防止useMemo
:計算結果のキャッシュ化
効率的なデバッグ手順
- React Developer Toolsのプロファイラー活用
- カスタムフックによる変更追跡
- 依存配列の適切な管理
これらの知識を身につけることで、「なぜ画面が更新されないのか」「どうすれば意図したタイミングで更新できるのか」といった日常的な疑問を解決できるようになります。また、不要な再レンダリングを防ぐことで、ユーザー体験の向上とアプリケーションの高速化を実現できます。
特に大規模なアプリケーション開発では、これらの最適化手法の習得が開発効率に直結します。最初は複雑に感じるかもしれませんが、実際のコードで試しながら理解を深めていくことで、より自信を持ってReact開発に取り組めるようになるでしょう。
今後の開発では、まず基本的な再レンダリングの仕組みを意識し、問題が発生した際は本記事で紹介したデバッグ手順を活用してください。そして、パフォーマンスが重要な箇所では適切な最適化手法を選択し、メンテナンスしやすいコードを心がけることが大切です。
React開発のスキルアップは継続的な学習と実践の積み重ねです。この記事が、あなたの開発効率向上と、より良いユーザー体験を提供するアプリケーション作成の一助となれば幸いです。

