React 18 から 19 へのアップデート時に注意すべきポイントまとめ

こんにちは、PB開発本部の宇野です。 現在のプロジェクトでは React 18 を使用していますが、今後どこかのタイミングで React 19 へのアップデートが必要になるはずです。スムーズに移行できるよう、React 19 へバージョンアップする際の注意点を事前に整理しておきました。

React 19 はレンダリング挙動の改善・新しい Actions・StrictMode 強化などがあり、React 18 のままのコードだと意図しない挙動になるケースがあります。

特に Vite + plugin-react-swc を使っている環境は、React 19 で最もトラブルが起きやすい組み合わせみたいです。

1. Vite + plugin-react-swc の相性問題(React 19 移行時)

React 19 にアップデートする際に最も注意すべきポイントのひとつが、
Vite と @vitejs/plugin-react-swc の相性問題です。

そもそも Vite とは?

Vite(ヴィート)は、フロントエンド開発のための高速ビルドツールです。

  • 開発サーバーが高速(HMR が速い)
  • ES Modules を活用した無駄のないビルド
  • React / Vue / Svelte などに幅広く対応
  • Next.js より軽量で設定もシンプル

React 19 と Vite + SWC がぶつかる理由

Vite で React を使う場合、次の2種類の公式プラグインがあるようです。

  1. @vitejs/plugin-react(Babel ベース・安定)
  2. @vitejs/plugin-react-swc(SWC ベース・高速だが互換性に弱い)

React 19 では 新しい JSX Runtime が導入されたが、
SWC の対応が遅れており、互換性問題が非常に出やすい状態みたいです。

よく発生するエラー例

react/jsx-runtime.js が見つからない
_jsx is not exported by react/jsx-runtime
JSX runtime is not supported by this version of SWC

このようなビルドエラーは、React 19 と SWC の runtime 仕様が噛み合っていないことで発生します。

対策① — plugin-react-swc を最新に更新する

まずは SWC プラグインそのものをアップデートします。

npm i -D @vitejs/plugin-react-swc@latest

最新バージョンでは徐々に React 19 対応が進んでいます。

ただし、それでも完全に解消しない場合があります。

対策② — plugin-react に切り替える(最も安定)

最も堅実なのは Babel ベースの plugin-react に切り替えることです。

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});

メリット:

  • React 19 の互換性が高い

  • JSX Runtime 関連のエラーが出ない

  • 公式ドキュメントでも plugin-react がデフォルト扱い

デメリット:

  • SWC よりビルド速度は少し遅い(とはいえ実用上は十分速い)

React のメジャーバージョンアップ直後は、 高速な SWC より Babel の方が安定しやすいという傾向があるようです。

2. StrictMode の副作用がより厳しくチェックされる

React 19 では StrictMode の副作用チェックがより強化されています。

その結果、開発モードでは以下のような処理が 2回実行されるパターンが増加 します。

useEffect(() => {
  console.log("実行");
}, []);

影響しやすいポイント

  • API が 2 回呼ばれる

  • WebSocket が二重接続される

  • setInterval が重複登録される

  • 初期化処理が 2 回動く

対策

  • 必ず cleanup を書く

  • API 呼び出しは一回だけになるよう制御する

  • 初期化処理はコンポーネント外へ移す

開発環境だけの挙動とはいえ、React 18 より厳しくなっている点に注意が必要です。

3. useEffect の実行タイミングが微妙に変わる

React 19 では concurrent rendering の改善により、useEffect の実行が React 18 より遅れるケースがあるみたいです。

特に影響が出やすいのは以下のようなものです。

  • DOM を参照している処理

  • 画面表示直後の scrollTo

  • UI ライブラリ(MUIなど)で DOM 計測をしているコード

対策:DOM 操作が必要な場合は useLayoutEffect を使用

useLayoutEffect(() => {
  window.scrollTo(0, 0);
}, []);

4. 新しい Actions API の挙動の違い

React 19 では Actions API(useOptimistic / useActionState)が追加され、状態の楽観的更新が簡単になるみたいです。 以下はよく使われるuseStateとの書き方の比較です。

useState

const [count, setCount] = useState(0);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState(null);

async function handleIncrement() {
  setIsPending(true);
  try {
    const result = await updateCountOnServer(count + 1);
    setCount(result);
  } catch (e) {
    setError(e);
  } finally {
    setIsPending(false);
  }
}

useActionState

「非同期アクションの状態管理」を React が自動で行ってくれるフック。 ローディング状態や最新値の更新までが一括で扱えるようです。

async function incrementAction(prevCount) {
  // サーバーに送信して新しい count を取得
  const newCount = await updateCountOnServer(prevCount + 1);
  return newCount;
}

const [count, increment, isPending] = useActionState(incrementAction, 0);

// ボタンから increment を呼ぶだけでOK
function handleIncrement() {
  increment();
}

useOptimistic

楽観的 UI(Optimistic UI)を簡潔に書くフック。 サーバー通信の結果を待たずに UI を先に更新し、失敗時は React が自動で戻してくれるそうです。

const [optimisticCount, addOptimisticCount] = useOptimistic(
  count,
  (current, delta) => current + delta
);

async function handleIncrement() {
  addOptimisticCount(1);         // UI 先行反映(楽観的)
  await updateCountOnServer(count + 1); // 成功すると実数値が確定
}

3つの違いまとめ

フック 役割 非同期処理 楽観的更新 ロールバック 主な用途
useState 基本的な state 管理 手動 手動実装 手動実装 通常の UI 状態
useActionState 非同期アクションの状態管理 React が管理 なし 自動 フォーム送信 / サーバー更新
useOptimistic 楽観的 UI 専用 任意 自動 自動 いいね / カウント / リスト更新

まとめ

React 19 は大幅な改善が入っており、パフォーマンスも API も進化していますが、React 18 のコードをそのまま移行すると細かい挙動の違いでハマる可能性があります。

特に注意すべきポイントは次の3つとなってます。

  1. Vite + plugin-react-swc の相性問題
  2. StrictMode の副作用チェックの強化
  3. useEffect のタイミング変化

React19 にバージョンを上げる際は、本記事で書いたことを参考にしてみてください。