【入門編】 React 18の自動バッチングと副作用の実行タイミング – React実践ガイド

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ライフをより豊かに、そして楽しくする一助となれば嬉しいです。もし、この記事を読んで「なるほど!」と思ったり、「ここがちょっと分かりにくかったな」という点があれば、ぜひコメントで教えてくださいね!

コメント

タイトルとURLをコピーしました