logo

flushSync で state 更新を同期的にフラッシュする

2025-11-21
a month ago

開発環境

  • react 19.2.0
  • react-dom 19.2.0

前提

React の state 更新は通常「非同期的」にバッチングされます。

そのため、setState の直後に DOM 操作を行うと、反映前の DOM を扱ってしまうケースがあります。

こうした「DOM を即座に扱いたい場面」で利用できるのが flushSync

flushSync を使うと、囲まれた state 更新が即時にフラッシュされ、同期的に DOM が更新されるようになります。

本題

どんな時に使うのか

  • 外部ライブラリや低レベル DOM API を叩く前に、最新の state・DOM を確実に反映させたい時
  • スクロール位置、サイズ測定、フォーカス制御など、描画後の DOM を前提とする処理が必要な時

React はレンダリングを最適化するため、複数の state 更新をまとめて処理しています。

しかし、即座に DOM が必要なケースではこの最適化が不都合になることもあります。

そこで以下のように flushSync を使うことで調整します。

実装サンプル:最新 DOM を前提としたスクロール制御

以下はメッセージ追加 → 直後にスクロールを最下部へ移動したい例

import { useRef, useState } from 'react';
import { flushSync } from 'react-dom';

export function Chat() {
  const [messages, setMessages] = useState<string[]>([]);
  const bottomRef = useRef<HTMLDivElement>(null);

  const addMessage = () => {
    // ここで flushSync を使うことで、messages が即時レンダーされる
    flushSync(() => {
      setMessages((prev) => [...prev, '新しいメッセージ']);
    });

    // DOM が確実に更新された後なので安全にスクロールできる
    bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  return (
    <div>
      <button onClick={addMessage}>追加</button>

      <div style={{ height: 200, overflowY: 'auto', border: '1px solid #ddd' }}>
        {messages.map((m, i) => (
          <p key={i}>{m}</p>
        ))}
        <div ref={bottomRef} />
      </div>
    </div>
  );
}

解説

  • flushSync(() => setMessages(...))により、このブロック内の state 更新は同期的に DOM に反映されます
  • そのためscrollIntoViewを実行する時点では DOM が確実に更新済み
  • React の通常レンダリングの非同期性で発生する「スクロール先がずれる・反映されない」問題を回避できます

注意点

  • 過度に使うと React のバッチング最適化が失われ、パフォーマンス低下の原因になります
  • 本当に「最新 DOM が必要な場面」に絞って利用するべきです

さいごに

flushSync は常用する API ではなく、DOM を同期的に扱いたい「特殊な状況」でのみ効果を発揮するものです。 React のレンダリングモデルを理解した上で、必要最小限で使うことで、外部ライブラリとの連携やフォーカス制御などを安全に実装できるようになります。

参照