React 18の「自動バッチング」って何? useEffectの動きがどう変わったのか、こっそり教えちゃいます!
「React 18って、なんか色々新しくなったらしいけど、具体的に何が変わったの?」
そんな疑問をお持ちのあなた、ようこそ!今日は、React 18になって「へぇ!」って思わず膝を打つ、でもちょっとだけ「あれ?」ってなりがちな「自動バッチング」という機能について、一緒に深掘りしていきましょう。特に、Reactで「副作用」を扱うときに大活躍する `useEffect` というフックの動きにどう影響するのか、じっくり見ていきますよ。
まずは「副作用」と「useEffect」のおさらいから…大丈夫、簡単だから!
Reactを触り始めたばかりの方、ちょっと復習したいな、という方、ご安心ください。専門用語はできるだけかみ砕いて、身近な例え話で説明しますね。
「副作用」って、悪いものじゃないんです!
「副作用」って聞くと、薬の副作用みたいに「なんか良くないこと?」って思っちゃいませんか? でも、Reactの世界での「副作用」は、ちょっと違うんです。
例えるなら、あなたがお店で欲しいものを見つけて、「これください!」って店員さんに言った後、
1. お会計をする
2. 商品を袋に入れてもらう
3. お店の外に出て、家に帰る
こういう一連の流れって、欲しいものを見つけるっていう「メインの目的」とは別に、「付随して起こる作業」ですよね。Reactでいう「副作用」も、コンポーネントが画面に表示されたり、更新されたりした「後に」、何か追加でやっておきたい処理のことを指します。
例えば、
- APIからデータを取ってくる(「商品一覧を表示して!」って言った後に、在庫情報を取ってくるイメージ)
- ブラウザのタイトルを変える(「このページを見せて!」って言った後に、ブラウザのタブの文字を変えるイメージ)
- タイマーを設定する(「このボタンを押したら、3秒後に何かする」みたいな)
こういう、画面の表示とは直接関係ないけれど、必要な処理のことを「副作用」と呼ぶんです。
`useEffect` は「副作用」の専門家!
そして、その「副作用」をうまく扱うために、Reactには `useEffect` という魔法の道具(フック)があるんです。
`useEffect` は、コンポーネントがレンダリング(画面に表示されること)された後に、指定した処理を実行してくれるんです。まるで、あなたが欲しいものを見つけて、お店で「さあ、次はお会計!」ってなるタイミングで、店員さんが「はい、こちら袋詰めです!」ってやってくれる、そんな感じです。
import React, { useState, useEffect } from ‘react’;
function Counter() {
const [count, setCount] = useState(0);
// useEffectは、コンポーネントがレンダリングされた「後に」実行される
useEffect(() => {
// ここに「副作用」の処理を書く
// 例:ブラウザのタイトルを更新する
document.title = `あなたは ${count} 回クリックしました`;
console.log(‘useEffectが実行されました!’);
// クリーンアップ関数(後で詳しく!)
return () => {
console.log(‘クリーンアップ実行!’);
};
}, [count]); // ← この [count] が超重要!(依存配列)
console.log(‘コンポーネントがレンダリングされました!’);
return (
クリック数: {count}
);
}
export default Counter;
このコードでは、ボタンをクリックするたびに `count` が増えます。`count` が増えるたびにコンポーネントが再レンダリングされるんですが、その後に `useEffect` の中の処理(ブラウザのタイトル更新や `console.log`)が実行されるんです。
さあ、本題!React 18の「自動バッチング」とは?
さて、いよいよ本題です。React 18になって、この `useEffect` の動きにちょっとした「隠し技」が加わりました。それが「自動バッチング」です。
「バッチング」って、何かの処理をまとめてやること?
「バッチング」って、聞くとちょっと難しそうですよね。でも、これも身近な例で考えてみましょう。
あなたがスーパーで買い物に行くとします。
- お菓子を1つカゴに入れる
- 飲み物を1本カゴに入れる
- パンを1つカゴに入れる
もし、一つ一つ「ピッ、ピッ」ってレジでスキャンしていたら、どうでしょう? 毎回「お会計ですか?」って聞かれて、その都度レジ係の方が作業する…ちょっと面倒ですよね。
そこで、多くのスーパーでは「まとめてお会計」というシステムがあります。カゴの中身を全部レジに持っていって、一度に全部スキャンして、一度に全部お会計しますよね。これが「バッチング」です。複数の処理を、まとめて一つにまとめることなんです。
React 18以前:バラバラだった「お会計」
React 18より前は、この「バッチング」が、ちょっと限定的でした。
例えば、イベントハンドラー(ボタンクリックとか)の中で `setState`(状態を更新すること)を複数回呼ぶと、Reactはそれを別々の処理として扱っていました。つまり、状態が更新されるたびに、コンポーネントが再レンダリングされて、その都度 `useEffect` も実行されていたんです。
// React 18 より前のイメージ
function MyComponent() {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const handleClick = () => {
setState1(prev => prev + 1); // 1回目のレンダリング(useEffectも実行)
setState2(prev => prev + 1); // 2回目のレンダリング(useEffectも実行)
};
useEffect(() => {
console.log(‘useEffectが実行されました!’);
}, [state1, state2]); // state1 または state2 が変わったら実行
return (
);
}
この場合、ボタンを1回クリックすると、`setState1` と `setState2` がそれぞれ呼ばれます。React 18より前だと、これは2回別々の「お会計」になるので、コンポーネントが2回レンダリングされ、それに伴って `useEffect` も2回実行されていたんです。ちょっと非効率ですよね。
React 18以降:賢くなった「まとめてお会計」!
ところが! React 18になって、この「バッチング」が自動で行われるようになったんです! これが「自動バッチング」です。
どういうことかというと、React 18では、イベントハンドラー内で複数回行われた `setState` を、まとめて1回のレンダリングに集約してくれるようになりました。
// React 18 以降のイメージ
function MyComponent() {
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const handleClick = () => {
setState1(prev => prev + 1);
setState2(prev => prev + 1);
// これで、まとめて1回のレンダリング!
};
useEffect(() => {
console.log(‘useEffectが実行されました!’);
}, [state1, state2]); // state1 または state2 が変わったら実行
return (
);
}
このコードは、React 18以降で実行すると、ボタンを1回クリックしても、`setState1` と `setState2` はまとめて1回のレンダリングとして扱われます。つまり、コンポーネントは1回だけ再レンダリングされ、`useEffect` も1回だけ実行されるようになったんです!
まるで、スーパーのレジで「まとめてお会計」ができるようになったみたいで、なんだか賢くなった感じがしますよね。
自動バッチングは `useEffect` の実行タイミングにどう影響する?
さて、ここからが `useEffect` との関わりが深くなるところです。
1. 実行回数が減る!
これが一番分かりやすい変化です。先ほどの例のように、イベントハンドラー内で複数の状態更新があった場合、React 18以降ではレンダリング回数とそれに伴う `useEffect` の実行回数が減ります。
- React 18以前: 状態更新の数だけレンダリング → `useEffect` もその回数だけ実行
- React 18以降: 複数の状態更新を1つのバッチに → 1回のレンダリング → `useEffect` も1回実行
これは、パフォーマンスの向上に直結します。無駄なレンダリングや `useEffect` の実行が減ることで、アプリケーションの動作が軽快になる可能性があるんです。
2. レンダリングサイクルとの関係
Reactのレンダリングサイクルは、ざっくり言うと「状態が更新される → コンポーネントが再レンダリングされる → DOMが更新される」という流れです。
`useEffect` は、このレンダリングサイクルが完了した「後」に実行される、というルールがありました。
React 18の自動バッチングは、この「状態が更新される → コンポーネントが再レンダリングされる」の部分を効率化しています。つまり、状態更新からDOM更新までのステップが、よりスムーズに、より少なくなるということです。
そして、そのレンダリングサイクルの完了後に、`useEffect` が実行される、という関係は変わりません。
「クリーンアップ関数」との関係は?
`useEffect` には、ときどき `return` で関数を返すことがありますよね。これが「クリーンアップ関数」です。
useEffect(() => {
// 何か処理を開始する(例: タイマーセット)
const timerId = setInterval(() => {
console.log(‘タイマー作動中…’);
}, 1000);
// 次のuseEffect実行前や、コンポーネントがアンマウントされる前に、
// 開始した処理を「片付ける」ための関数
return () => {
console.log(‘クリーンアップ!タイマーを止めます。’);
clearInterval(timerId); // タイマーを止める
};
}, []); // 依存配列が空なので、初回レンダリング時のみ実行
このクリーンアップ関数は、
- 次の `useEffect` が実行される前
- コンポーネントが画面から消える(アンマウントされる)時
に実行されます。
React 18の自動バッチングによって、`useEffect` の実行回数が減った場合、それに伴ってクリーンアップ関数が実行される回数も減る可能性があります。これは、特にタイマーやイベントリスナーなどを設定している場合に、リソースの解放が効率的に行われることを意味します。
どこで「自動バッチング」が効くの?効かないの?
ここ、ちょっと重要なので、しっかり見ていきましょう!
自動バッチングが効く場所
React 18の自動バッチングは、主に以下の場所での状態更新に適用されます。
- Reactイベントハンドラー内: `onClick`, `onChange` などのイベント発生時の処理
- setTimeout, setInterval 内: ブラウザのタイマー処理
- Promise の `.then()` 内: 非同期処理の結果を受けての状態更新
あれ? `setTimeout` とか `Promise` って、イベントハンドラーじゃないのに効くの? って思いますよね。そうなんです、React 18の「自動バッチング」は、こうした「Reactの外部」からトリガーされる更新にも、賢く対応してくれるようになったんです。
自動バッチングが効かない場所(React 18以前と同じ挙動)
一方で、以下の場所での状態更新は、React 18でも個別にレンダリングされます(自動バッチングは適用されません)。
- Promise の `catch()` ブロック内
- `requestAnimationFrame` 内
- ネイティブのイベントリスナー内 (`document.addEventListener` など)
なぜかというと、これらの処理はReactのレンダリングサイクルとは直接的な関係が薄かったり、予測不能なタイミングで発生したりすることが多いため、Reactが「これはまとめてやっちゃダメだ!」と判断しているからです。
「root.render」 を使っているかどうかも重要!
React 18で自動バッチングの恩恵を受けるには、新しいRoot API (`createRoot`) を使ってアプリケーションを起動していることが前提となります。
もし、まだ古い `ReactDOM.render` を使っている場合は、React 18の機能(自動バッチングを含む)は有効になりません。
// React 18 以降の推奨される起動方法
import React from ‘react’;
import ReactDOM from ‘react-dom/client’; // ← この import が新しい
import App from ‘./App’;
const root = ReactDOM.createRoot(document.getElementById(‘root’)); // ← createRoot を使う
root.render(
);
// 古い方法(React 17以前)
// import ReactDOM from ‘react-dom’;
// ReactDOM.render(…)
もし、お使いのプロジェクトがまだ古い `ReactDOM.render` を使っている場合は、React 18へのアップデートと合わせて、この `createRoot` への移行も検討してみてくださいね。
まとめ:React 18の自動バッチングは、 `useEffect` をもっと賢く、もっと効率的にしてくれる!
今日は、React 18の「自動バッチング」と、それが `useEffect` の実行タイミングにどう影響するのかを見てきました。
- 自動バッチング: React 18から、イベントハンドラーなどでの複数の状態更新を、まとめて1回のレンダリングに集約してくれる賢い仕組み。
- `useEffect` への影響:
- 実行回数が減る: パフォーマンス向上に貢献!
- レンダリングサイクルが効率化: よりスムーズな画面更新に。
- クリーンアップ関数も効率的に: リソース管理がスムーズに。
- 適用される場所: Reactイベントハンドラー、`setTimeout`、Promiseの`.then()`など。
- 注意点: 古い `ReactDOM.render` を使っていると効かない。
この自動バッチングのおかげで、私たちは以前よりも少ないコードで、より効率的にReactアプリケーションを開発できるようになりました。
「あれ? `useEffect` の実行回数が減ったな?」
もし、そんな風に感じたら、それはReact 18の自動バッチングが頑張ってくれている証拠かもしれません。
Reactは常に進化しています。今日の話が、皆さんのReactライフをより豊かに、そして楽しくする一助となれば嬉しいです。もし、この記事を読んで「なるほど!」と思ったり、「ここがちょっと分かりにくかったな」という点があれば、ぜひコメントで教えてくださいね!

コメント