React-非同期処理内のレンダリングについて

今回は、プロジェクトのメンバーとディスカッションした内容を理解するために深くみていきたいと思います。

内容としては、setState(ステート更新)とawait(非同期処理を待つ)をしている非同期のイベントハンドラーがあり、setStateによるレンダリングのタイミングについてです。

Reactアプリケーションを開発していると、状態更新とレンダリングのタイミングについて悩むことがよくありますよね。それでは、Reactの状態更新の仕組みをJavaScriptの実行コンテキストやイベントループと合わせて理解するために説明していきたいと思います。

非同期処理時の状態更新とレンダリングタイミングについて

まず、以下のような非同期処理を含むコードを見てみましょう。

const testFunction = async () => {
    setState('test');
    await API();
}

このコードのレンダリングタイミングを理解するためには、以下の3つの重要な概念を押さえる必要があります。

  1. Reactのバッチ処理
  2. JavaScriptの実行コンテキスト
  3. イベントループ

それでは、それぞれ詳しく見ていきたいと思います。

Reactのバッチ処理(Batching)

Reactのバッチ処理は、パフォーマンス最適化のための重要な機能です。複数の状態更新をまとめて1回のレンダリングにすることで、不要なレンダリングを防ぎます。

const handleClick = () => {
  setCount(c => c + 1);    // 更新1
  setName("Alice");        // 更新2
  setAge(a => a + 1);     // 更新3
  // この3つの更新は1回のレンダリングにまとめられる
}

React 18からは「自動バッチ処理」が導入され、非同期処理内でも自動的にバッチ処理が行われるようになりました。これにより、より効率的な状態更新が可能になっています。

ちなみに、Reactバッチ処理がないと上記のサンプルでは3回、レンダリングされてしまい、パフォーマンスが良くないです。

JavaScriptの実行コンテキスト

実行コンテキストは、コードが実行される環境を定義する重要な概念です。主に以下の3種類があります。

  • グローバル実行コンテキスト
    • スクリプト全体で共有される環境
    • windowオブジェクトなどが含まれる
  • 関数実行コンテキスト
    • 関数が呼び出されるたびに作成される独立した環境
    • ローカル変数やスコープ情報を保持
  • eval実行コンテキスト
    • eval関数内で使用される特殊な環境

イベントループ

JavaScriptのイベントループは、非同期処理を管理する中核的な仕組みです。主に以下のコンポーネントで構成されています。

  • コールスタック
    • 同期的な処理が積まれる場所
    • 後入れ先出し(LIFO)で処理される
  • タスクキュー(マクロタスク)
    • setTimeout, setInterval, UIイベントなどの処理が入る
    • コールスタックが空になってから処理される
  • マイクロタスクキュー
    • Promise, process.nextTickなどの処理が入る
    • 各マクロタスクの後に処理される

実際の処理順序を見てみましょう。

console.log('1');  // 同期処理

setTimeout(() => {
  console.log('2');  // マクロタスク
}, 0);

Promise.resolve().then(() => {
  console.log('3');  // マイクロタスク
});

console.log('4');  // 同期処理

// 出力順序: 1 → 4 → 3 → 2

すべてを組み合わせて理解する

これらの概念を組み合わせると、冒頭の非同期処理のレンダリングタイミングが理解できます。

  1. testFunctionが呼び出され、新しい実行コンテキストが作成される
  2. setState('test')が実行され、状態更新がスケジュールされる
  3. await API()で関数の実行が一時停止し、実行コンテキストから抜ける
    • この時点でイベントループに制御が戻る
    • Reactのバッチ処理が実行され、レンダリングが発生
  4. API処理が完了後、関数の残りの処理が実行される

このプロセスを視覚化するために、デバッグ用のコードを書いてみましょう。

const TestComponent = () => {
  const [state, setState] = useState('initial');

  const testFunction = async () => {
    console.log('Before setState');
    setState('test');
    console.log('After setState, before await');
    await API();
    console.log('After await');
  };

  console.log('Render:', state);

  return <div>{state}</div>;
};

このコードを実行すると、以下のような順序でログが出力されます。

Render: initial
Before setState
After setState, before await
Render: test
After await

まとめ

Reactの非同期処理とレンダリングは、一見複雑に見えますが、以下の要素を理解することで整理できます。

  • Reactのバッチ処理により、複数の状態更新が最適化される
  • 実行コンテキストの切り替わりにより、awaitの時点でレンダリングが発生する
  • イベントループが非同期処理全体を制御する

コメントを残す