12k
All articles

preact/compat を使って Preact で React ライブラリを動かす

preact/compatでReactライブラリをPreactで動かす方法を解説。Vite、webpack、Rollup、Jest、TypeScriptのalias設定と失敗例を紹介。

OpenReplay Team
OpenReplay Team
preact/compat を使って Preact で React ライブラリを動かす

preact/compat は互換レイヤーであり、Preact X 以降はメインの preact パッケージに同梱されています。React の公開 API を Preact にマッピングすることで、ほとんどの React ライブラリを変更なしに動作させつつ、アプリにバンドルされるフレームワークは React のはるかに大きなランタイムではなく約 9.5KB で済みます。

エイリアスを差し替えるだけなら 5 分で終わる作業です。厄介なのは、3 日後に node_modules の奥深くから日付ピッカーがエラーを投げていることに気づく、といった部分で、これについては誰も警告してくれません。有効化するには、バンドラー側で reactreact-dompreact/compat にエイリアスするだけです。コンポーネントのコード変更も、別パッケージのインストールも不要です。本ガイドでは、主要な各ツールチェーン向けの正確なエイリアス設定、バンドルサイズ削減効果を含む移行手順、そしてどのライブラリが壊れるのかについての率直な説明を紹介します。

要点

  • preact/compatpreact パッケージに同梱されています。compat はコアに取り込まれたため、スタンドアロンの preact-compat パッケージは廃止されておりnpm install preact だけで十分です。
  • 仕組みの全体は、reactreact-domreact-dom/test-utilsreact/jsx-runtime という 4 つの import パスを Preact に向けるエイリアス設定です。
  • @preact/preset-vite を使えばエイリアス設定は自動で行われるため、resolve.alias を手書きする必要はありません。
  • webpack では react-dom のエイリアスを react-dom/test-utils より下に記述する必要があります。そうしないと、より広範なルールが test-utils のマッピングを覆い隠してしまいます。
  • compat がカバーするのは React の公開 API であり、内部実装ではありません。react-dom の深い内部パスに手を伸ばすライブラリや、最新の React 19 API に依存するライブラリは依然として動作しない可能性があります。

preact/compat とは何か、なぜ存在するのか

preact/compat は、React の公開 API 表面(React.Component、hooks、createPortalforwardRefmemo、JSX ランタイム)を Preact の同等機能に変換します。これにより、React 向けに書かれたサードパーティ製コンポーネントがビルド時に Preact へ解決されます。Preact の npm ページ では、単一のエイリアスによる幅広い React サポートを謳っており、この互換性こそが書き直しなしに React エコシステムを再利用できる理由です。

インストールすべき preact-compat パッケージはもう存在しません。公式アップグレードガイドによれば、このレイヤーはかつて独立して提供されていましたが、調整を簡素化するためにコアリポジトリへ統合されました。したがって、バージョンを上げる場合は古い preact-compat の import とエイリアスを preact/compat に置き換える必要があります。スコープなしのパッケージは行き止まりです。その GitHub リポジトリは 2021 年 12 月からアーカイブされ読み取り専用となっており、npm ページにはアンインストールするよう記載されています。Preact X はデフォルトで compat を同梱しているためです。現在の安定版ラインは Preact 10.x で、11.0.0 は一般提供ではなくリリース候補段階にあります。正確なバージョン番号は Preact のリリースページで確認できます。

エイリアス設定がすべて

仕組みの全体はエイリアス設定です。reactreact-domreact-dom/test-utilsreact/jsx-runtime を Preact に向けることで、node_modules の奥深くにあるサードパーティライブラリを含め、既存のすべての import が React ではなく preact/compat に解決されます。コンポーネントのコードは何も変わりません。import { useState } from 'react' という記述はそのまま残り、バンドラーが react の解決先を書き換えます。

Preact の React を Preact にエイリアスするガイドに基づく、4 つの標準的なエントリは以下のとおりです。

Import パスエイリアス先理由
reactpreact/compatReact のコア API
react-dom/test-utilspreact/test-utilsテストユーティリティ
react-dompreact/compatDOM レンダラー(test-utils よりに置く必要あり)
react/jsx-runtimepreact/jsx-runtime自動 JSX トランスフォーム

古いチュートリアルでよく見られる reactreact-dom だけをエイリアスするショートカットでは、JSX ランタイムや react-dom/test-utils を import するライブラリが React に解決されたままとなり、削減しようとしていたバイト数が再び戻ってきてしまいます。

ツールチェーン別のエイリアス設定

Vite(推奨デフォルト)

@preact/preset-vite を使えばエイリアス設定は自動で行われるため、resolve.alias を手書きすべきではありません。プリセットが React 向けエイリアスを有効化します。これは reactAliasesEnabled オプションで制御され、明示的に無効化しない限り true に設定されます。JSX トランスフォームの設定も同時に行われます。

// vite.config.ts
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

export default defineConfig({
  plugins: [preact()], // JSX + react→preact/compat aliasing handled automatically
});

プリセットなしで Vite を実行する場合は、手動のフォールバックを追加します。

export default defineConfig({
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-dom': 'preact/compat',
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
});

Webpack

webpack では react-dom のエイリアスを react-dom/test-utils より下に記述する必要があります。そうしないと、より広範な react-dom のルールが test-utils のマッピングを覆い隠し、テストユーティリティが警告なく誤ったモジュールに解決されてしまいます。

const config = {
  resolve: {
    alias: {
      react: 'preact/compat',
      'react-dom/test-utils': 'preact/test-utils',
      'react-dom': 'preact/compat', // Must be below test-utils
      'react/jsx-runtime': 'preact/jsx-runtime',
    },
  },
};

Rollup

@rollup/plugin-alias をインストールし、@rollup/plugin-node-resolveに登録します。これにより、Rollup がモジュールを解決する前に書き換えが行われます。

import alias from '@rollup/plugin-alias';

export default {
  plugins: [
    alias({
      entries: [
        { find: 'react', replacement: 'preact/compat' },
        { find: 'react-dom/test-utils', replacement: 'preact/test-utils' },
        { find: 'react-dom', replacement: 'preact/compat' },
        { find: 'react/jsx-runtime', replacement: 'preact/jsx-runtime' },
      ],
    }),
  ],
};

Node / Next.js(バンドラーのエイリアスが効かない場合)

Node ランタイムはバンドラーのエイリアスを無視します。Next.js も同様です。そのため、公開されている @preact/compat パッケージを使い、package.json にエイリアスを記述します。このスコープ付きパッケージは、npm 組み込みのエイリアス機能が指し示す対象を用意するためだけに存在し、その役割は preact/compat をそのまま re-export することだけです。スコープ付きの @preact/compat は、廃止されたスコープなしの preact-compat とは別物である点に注意してください。

{
  "dependencies": {
    "react": "npm:@preact/compat",
    "react-dom": "npm:@preact/compat"
  }
}

Jest

Jest は moduleNameMapper の正規表現エントリでモジュールパスを書き換えます。

{
  "moduleNameMapper": {
    "^react$": "preact/compat",
    "^react-dom/test-utils$": "preact/test-utils",
    "^react-dom$": "preact/compat",
    "^react/jsx-runtime$": "preact/jsx-runtime"
  }
}

TypeScript

TypeScript はバンドラーとは独立して型を解決するため、tsconfig.json でパスをマッピングし、skipLibCheck を有効にします。skipLibCheck を有効にする理由は、一部の React ライブラリが compat の提供しない型に依存しており、node_modules 内のすべての .d.ts をフルチェックするとそれらの宣言で失敗するためです。

{
  "compilerOptions": {
    "skipLibCheck": true,
    "baseUrl": "./",
    "paths": {
      "react": ["./node_modules/preact/compat/"],
      "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
      "react-dom": ["./node_modules/preact/compat/"],
      "react-dom/*": ["./node_modules/preact/compat/*"]
    }
  }
}

最小限の移行手順

既存の React アプリをモダンなツールチェーン上で Preact に移行する作業は、4 つのステップで完了します。

  1. 依存関係を差し替える。 reactreact-dom とそれらの @types を削除します(Preact は独自の TypeScript 型定義を同梱しているため)。続いて npm install preactnpm install -D @preact/preset-vite を実行します。
  2. プリセットを追加する。 Vite のプラグインに preact() を追加します。JSX トランスフォームと react → preact/compat のエイリアスの両方を処理してくれるため、Vite 2 時代の設定で手書きしていた esbuild.jsxInject / jsxFactory の設定は削除できます。
  3. レンダリングのエントリポイントを変更する。 React DOM のマウント呼び出しを Preact の render に置き換えます。
// Before
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));

// After
import { render } from 'preact';
render(<App />, document.getElementById('root'));
  1. ビルドしてサイズを確認する。 得られる効果こそが目的です。Preact コアと preact/compat を合わせておよそ 9.5KB(min+gzip)に収まるのに対し、React 19 と React DOM は 60KB 近くになり、そのほとんどが react-dom/client エントリによるものです。コンポーネントがすでに compat 経由で解決されているアプリにとって、この差分はほぼコストゼロで得られます。

preact/compat が壊れるのはどんなときか

compat がカバーするのは React の公開 API であり、内部実装ではありません。プライベートな react-dom の内部パスに手を伸ばすライブラリや、比較的新しい React 19 の API に依存するライブラリは、エイリアスが正しくても壊れる可能性があるため、リリース前に各依存関係を検証してください。React 用の型定義を持つライブラリによる型の不一致は想定内であり、skipLibCheck で対処できます。注意すべきはランタイムでの失敗であり、こうした統合部分のセッションリプレイでは、自分のコードではなく依存関係の内部から投げられたコンソールエラーとして頻繁に表面化します。

ライブラリを採用する前の簡単な事前チェックは以下のとおりです。

  • パッケージを grep して、react-dom/ の深い内部 import がないか確認します。これが最も一般的な破損のシグナルです。
  • ライブラリが依存している React 19 専用 API がないか確認します。憶測ではなく、現在の Preact リリースでのカバー状況を検証してください。
  • 上記の Jest moduleNameMapper の下でライブラリ自身のテストスイートを実行し、早い段階で失敗を検出します。
  • 開発環境でスモークテストを行い、依存関係の内部で発生するエラーがないかコンソールを監視します。

SSR やフレームワークの利用者は、別種の問題に直面します。Node ではバンドラーのエイリアスが適用されないため、Next.js などのランタイムでは package.json によるエイリアスが必要になります。また Vite の ssrLoadModule の経路では一部のエイリアス設定がバイパスされることがあるため、クライアント側とサーバー側の両方が preact/compat に解決されているか確認してください。

利用しているツールチェーンで 4 つのエントリをエイリアスし、同じマッピングを通して依存関係のテストを実行すれば、わずかなバイト数で React エコシステムの大部分を再利用できます。率直に言って例外となるのは、React の公開 API ではなく内部実装に結び付いたライブラリです。まずはブランチに @preact/preset-vite を追加し、本番バンドルのサイズを前後で計測することから始めましょう。

FAQ

@preact/compat と旧 preact-compat パッケージの違いは何ですか?

スコープ付きの @preact/compat は preact/compat を re-export する現行の npm パッケージで、バンドラーのエイリアスが適用されない Next.js などの Node ランタイムにおいて、package.json 経由で react をエイリアスする目的だけに使われます。一方、スコープなしの preact-compat はまったく別のアーカイブ済みパッケージで、リポジトリは 2021 年 12 月から読み取り専用です。これは Preact 8.x を対象としたもので、Preact X では compat がコアに同梱されています。スコープなしのものは決してインストールしないでください。

@preact/preset-vite を使う場合でも resolve.alias を手書きする必要はありますか?

いいえ。@preact/preset-vite を使えば、react と react-dom を preact/compat にエイリアスする処理は自動で行われます。これは reactAliasesEnabled オプションで制御され、明示的に無効化しない限り有効です。Vite のプラグインに preact() を追加すれば JSX トランスフォームとエイリアスの両方が処理されるため、resolve.alias を手書きするのは冗長であり、競合を招く可能性もあります。手動で 4 エントリのエイリアスブロックを書くのは、プリセットなしで Vite を実行する場合だけです。

webpack でエイリアスを設定した後、テストユーティリティが誤ったモジュールに解決されるのはなぜですか?

webpack の設定で react-dom のエイリアスが react-dom/test-utils より上に記述されているためです。webpack はより広範な react-dom のルールを先にマッチさせるので、より具体的な test-utils のマッピングが覆い隠され、テストユーティリティが警告なく preact/test-utils ではなく preact/compat に解決されてしまいます。react-dom のエントリを react-dom/test-utils より下に配置することで解決できます。Rollup にも同様の順序ルールがあり、@rollup/plugin-alias を @rollup/plugin-node-resolve より前に置く必要があります。

preact/compat のエイリアスが正しいのに React ライブラリが壊れるのはなぜですか?

compat がマッピングするのは React の公開 API であり、内部実装ではないためです。react-dom の深い内部パスを import するライブラリや、最新の React 19 API に依存するライブラリは、エイリアスが正しくてもランタイムで失敗することがあります。これらは自分のコードではなく依存関係の内部から投げられるコンソールエラーとして表面化します。ライブラリを採用する前に、深い react-dom/ の import がないか grep し、Jest の moduleNameMapper の下でそのライブラリ自身のテストスイートを実行してください。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.