---
title: SPAにしないReact移行
tags: 
author: [murasuke](https://docswell.com/user/4962106)
site: [Docswell](https://www.docswell.com/)
thumbnail: https://bcdn.docswell.com/page/47MYYDQN7W.jpg?width=480
description: サンプルソース https://github.com/murasuke/isolated-react
published: May 09, 26
canonical: https://docswell.com/s/4962106/ZX2W34-2026-05-09-153830
---
# Page. 1

![Page Image](https://bcdn.docswell.com/page/47MYYDQN7W.jpg)

SPAにしない React移行
Shadow DOMで15,000行のCSS地獄を隔離し、
レガシー環境と安全に同居する実装パターン


# Page. 2

![Page Image](https://bcdn.docswell.com/page/P7R99484E9.jpg)

自己紹介： murasuke
murasuke(むらすけ) 株式会社ツールラボ 開発部 部長
SE/プログラマーとして20年、多種多様なシステムを構築
プログラムが書きたいので転職しました (現職)
1年前、人生初のイベント参加(PHPカンファレンス名古屋)を
きっかけにして、初登壇となりました


# Page. 3

![Page Image](https://bcdn.docswell.com/page/PJXQQ28Z7X.jpg)

前提：１０年運用してきたレガシー環境
● CakePHP2
●
jQuery
●
独自CSS
10年以上にわたる追加開発、止められないレガシー環境
開発速度、開発体験ともに「なんとなく」悪化している
最新のフロントエンド開発環境が気にはなっている
なんとかしたい、でも作り直す余裕はない
それが現実ではないでしょうか？


# Page. 4

![Page Image](https://bcdn.docswell.com/page/3JK99MKVJD.jpg)

会場の皆様に質問です
●
jQuery
をバリバリ使っている方、どのくらいいますか？？
●
独自CSS
手書きCSSで頑張っている方、どのくらいいらっしゃいますか？
（仲間がいるとうれしいです）


# Page. 5

![Page Image](https://bcdn.docswell.com/page/LE3WWY3QE5.jpg)

このセッションのゴール
本日、伝えたいこと
● SPAではない React という選択肢がある
● React とレガシーの共存は可能。段階移行ができる
● Shadow DOM はピーキーだけど、強力な味方
と、その実現までの経緯をお話しします


# Page. 6

![Page Image](https://bcdn.docswell.com/page/8EDKK54W7G.jpg)

つまり、何をしたか？
❌ SPAにしない
❌ フルリプレースもしない
🔴 レガシーシステムに Reactを無理なく後付けする
というお話です


# Page. 7

![Page Image](https://bcdn.docswell.com/page/V7PKKGMXJ8.jpg)

開発体験を損なっていた「３つの負債」
●
スタイルの負債
15,000行のスパゲッティCSS、!importantで殴り合い
●
環境の負債
10年熟成したPHP/CakePHP、JSファイル間の暗黙の依存関係、型のない言語
●
心理的負債
リリースすると「どこかが壊れる」という恐怖による開発の鈍化
SPA化や、フルリプレースするほどの余裕もない・・・


# Page. 8

![Page Image](https://bcdn.docswell.com/page/2JVVV693JQ.jpg)

「フルリプレース」、「完全 SPA化」以外の道
「 フルリプレース 」でも「 完全SPA化」でもない、
レガシー環境を残しつつ、フロントエンド環境を追加する
「新旧共存」 という道 は取れないだろうか？


# Page. 9

![Page Image](https://bcdn.docswell.com/page/5EGLLNZYJL.jpg)

「React」をWebページの一部に組み込むという発想の転換
完全なSPA化は目指さず、コンテンツ領域だけを React に置き換える
Before
After


# Page. 10

![Page Image](https://bcdn.docswell.com/page/4JQYYQL67P.jpg)

「React」だけど SPAじゃない
・Viewでhtmlを生成するかわりに、React の初期化コードに置き換え
・画面遷移は、今まで通り「サーバー側」のルーティングを利用


# Page. 11

![Page Image](https://bcdn.docswell.com/page/K74WWNDME1.jpg)

ここまでのまとめ
● 既存Webページの一部にReactを埋め込む
○
●
画面表示時に、React Componentを描画する
SPAにしない（URLをクリックしたらサーバー側で画面を切り替え）
○
メニュー等、既存の画面遷移(URL)も変更不要
この手法なら、既存Webアプリはそのままで「 Reactと共存」できます！


# Page. 12

![Page Image](https://bcdn.docswell.com/page/LJ1YY5ZYEG.jpg)

React共存環境の課題
● 最大の課題
○ 15,000行 のスパゲッティ CSS が、Reactに侵入
● Reactを部分導入する構成ならではの課題
○
Reactに初期値(サーバー側変数)を効率よく引き渡す方法
○
SPAではない形でReactを利用するためのビルド手順


# Page. 13

![Page Image](https://bcdn.docswell.com/page/GJWGG59172.jpg)

15,000行のスパゲッティ CSS がReactに侵入する
既存CSSがReact領域に侵入し、Reactのスタイルが破壊される
これを解決しないことには、実践投入できません・・・


# Page. 14

![Page Image](https://bcdn.docswell.com/page/4EZLLN9X73.jpg)

スパゲッティ CSSの侵攻を防ぐには？
CSSの世界を分離するため、様々な手段を検討しました
Reactのページに既存 CSS
を読み込ませない
React領域に限定した
リセット CSS
iframe
メニューや画面全体の構成が崩れ
るので、読み込まないという選択
肢は取れない
一般的なリセットCSSは
!important に負けてしまう
無理やり上書きをすると
React側にも影響する
画面のちらつき、サイズ調整が困
難。画面とは別にiframe用の
URLが追加必要で、既存画面の
移行が複雑化する
いずれも決定打にならないので、iframeのようにCSSの世界を分離しつつ
同一のブラウザ内に表示する 、それを可能にする技術を探しました


# Page. 15

![Page Image](https://bcdn.docswell.com/page/Y76WW8KP7V.jpg)

救世主： Shadow DOM
Shadow DOM とは
●
画面内に独立したDOMツリー
を作成する
●
互いにスタイルやスクリプトが
干渉しないようにする仕組み
Reactと外部の世界を隔離する
まさに求めていた技術でした


# Page. 16

![Page Image](https://bcdn.docswell.com/page/G75MM9PP74.jpg)

Shadow DOM の基本的な使い方
attachShadow()で作成したDOMに要素を追加します
①
class=”box” を「赤い枠線」で囲むStyle定義
②
attachShadow() でShadow Root を作成
③
&lt;div class=”box”&gt;をShadow Rootに追加
●
Shadow Root内部は、外部と独立するので、
CSS (.box) が効きません！
&lt;html lang=&quot;ja&quot;&gt;
&lt;head&gt;
&lt;style&gt;
.box { /* ① */
background: #ffe6e6; /* 薄い赤背景 */
outline: 4px solid red; /* 赤く太い外枠 */
padding: 10px;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&quot;box&quot;&gt;Shadow DOMの外部です！&lt;/div&gt;
&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;script&gt;
const root = document.getElementById(&quot;root&quot;);
// ② Shadow DOM作成
const shadow = root.attachShadow({ mode: &quot;open&quot; });
// ③ Shadow DOM内にdivを追加
const box = document.createElement(&quot;div&quot;);
box.className = &quot;box&quot;;
box.textContent = &quot;こんにちは、Shadow DOM です！&quot;;
shadow.append(box);
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;


# Page. 17

![Page Image](https://bcdn.docswell.com/page/9J29926ZER.jpg)

Shadow DOM を使って、 Reactの独立性を向上させる
Shadow DOMを生成し、内部に「children」を描画する
&lt;IsolatedScope&gt;コンポーネントで、React領域を外部 CSSから守ります


# Page. 18

![Page Image](https://bcdn.docswell.com/page/DEY44Y9NJM.jpg)

&lt;IsolatedScope&gt;コンポーネント
Shadow DOM内にReact(children)を描画する
&lt;IsolatedScope&gt;コンポーネント
①
useRefで本物のDOMを取得
②
①のDOMに、Shadow DOMを作成
③
Shadow DOM内部にReact Rootを作成
④
Shadow DOM内にCSSを適用するため の
&lt;link&gt;を生成（ビルド済みCSSのURL）
⑤
React Rootにchildren(子コンポーネント)を
描画(render())する
const IsolatedScope = ({children, styleSheetURLs}) =&gt; {
const shadowHostRef = useRef&lt; HTMLDivElement | null&gt;(null);
const rootRef = useRef&lt; ReactDOM .Root | null&gt;(null); // ①
useEffect (() =&gt; {
if (!shadowHostRef.current) return;
// ② Shadow DOMの作成（初回のみ）
const shadowRoot = shadowHostRef.current.shadowRoot ||
shadowHostRef.current. attachShadow ({mode: &#039;open&#039;});
// ③ React Rootは一度だけ生成（エラー回避）
if (!rootRef.current) {
rootRef.current = ReactDOM. createRoot (shadowRoot);
}
rootRef.current. render( // ⑤
&lt;div id=&quot;shadow-root &quot;&gt;
{ /* ④ Shadow DOM内に適用するCSSを読み込む */ }
{styleSheetURLs ?.map((url) =&gt; (
&lt; link rel=&quot;stylesheet &quot; key={url} href={url} /&gt;
))}
{ /* ⑤ Shadow DOM内にReactコンポーネントを表示する */ }
&lt;div id=&quot;shadow-component-container &quot;&gt;{children}&lt;/ div&gt;
&lt;/div&gt;
);
}, [children]);
return &lt;div id=&quot;isolated-scope-host &quot; ref={shadowHostRef } /&gt;; /* ① */
};


# Page. 19

![Page Image](https://bcdn.docswell.com/page/VJNYYPLV78.jpg)

&lt;IsolatedScope&gt;の使い方
&lt;IsolatedScope&gt;で描画したいReactをラップして、埋
め込みたい位置へrender()します
①
②
③
サーバー側変数をjsonエンコードして、propsに
セット(※1)
&lt;script type=&quot;module&quot;&gt;
import {
ReactDOMClient,
Counter
} from &#039;/js/react/bundle-react.js&#039;;
// ① サーバー変数をjsonに変換してコードを埋め込む(props)
const props = &lt;?= json_encode([&#039;value&#039; =&gt; $value]) ?&gt;;
React Rootを作成
&lt;IsolatedScope&gt;をrender()
・Reactに適用するcssのパスを渡す
・描画するコンポーネントをラップする
・初期表示用propsも埋め込む
※1: HTML内にpropsデータを埋め込むのは、
初期表示を高速化するためです
(ロード時の非同期fetchを回避)
// ②
const container = document.getElementById(&#039;root&#039;);
const root = ReactDOMClient.createRoot(container);
// ③
root.render(
&lt;IsolatedScope styleSheetURLs=&quot;/css/react/bundle.css&quot;&gt;
&lt;Counter {...props} /&gt;
&lt;/IsolatedScope&gt;
);
&lt;/script&gt;
&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;


# Page. 20

![Page Image](https://bcdn.docswell.com/page/YE9PP34VJ3.jpg)

実際に使ってみてわかった不具合
Shadow DOMはかなり良い仕事をしてくれましたが、
いくつか想定外の不具合が発生しました
1.
「文字色、フォントサイズ などの継承プロパティ 」はShadow DOMでブロックできない
（&lt;head&gt;で定義したスタイルがReactに適用されてしまうが、これは仕様）
2.
Shadow DOM内でTailwindの一部スタイルが正しく適用されない
⇒ CSS変数 (カスタムプロパティー) が未定義になってしまう
3.
ダイアログが正しく表示されない （フロート表示にならない）
⇒ shadcn/ui ライブラリを利用した場合に発生


# Page. 21

![Page Image](https://bcdn.docswell.com/page/GE8DDMQ4ED.jpg)

CSS継承プロパティ がブロックされない問題と対策


# Page. 22

![Page Image](https://bcdn.docswell.com/page/LELMM3XV7R.jpg)

Tailwindのスタイルが正しく適用されない問題と対策


# Page. 23

![Page Image](https://bcdn.docswell.com/page/4JMYYDLNJW.jpg)

ダイアログが正しく表示されない問題と対策


# Page. 24

![Page Image](https://bcdn.docswell.com/page/PJR994K479.jpg)

不具合対策済み &lt;IsolatedScope&gt;
長いので、GitHubで公開します
// IsolatedScope.tsx
const IsolatedScope = ({children, styleSheetURLs, onPortalContainer})
const shadowHostRef = useRef&lt; HTMLDivElement | null&gt;(null);
const rootRef = useRef&lt; ReactDOM .Root | null&gt;(null);
引き渡されたComponentをShadowDOM内に描画する共通関数
=&gt; {
useEffect (() =&gt; {
if (!shadowHostRef.current) return;
// Shadow DOMの作成（初回のみ）
const shadowRoot = shadowHostRef.current.shadowRoot ||
shadowHostRef.current. attachShadow ({mode : &#039;open&#039;});
export function reactShadowDOMRenderer&lt;T&gt;(
rootId: string,
Component: React.FC&lt;T&gt;,
props: T = null,
styleSheetURLs: string[] = []
){
const onPortalContainer = (container: HTMLElement) =&gt; {
// portalのコンテナが作成されたときに呼び出される
// ダイアログなどportalを利用するコンポーネントに渡す必要がある
window[&#039;portalContainer&#039;] = container;
};
// 継承スタイルを遮断してスコープを分離
const sheet = new CSSStyleSheet ();
sheet. replaceSync (`:host {all: initial;} `);
shadowRoot.adoptedStyleSheets = [sheet];
const container = document.getElementById(rootId);
const root = ReactDOMClient.createRoot(container);
root.render(
&lt;IsolatedScope styleSheetURLs={styleSheetURLs} onPortalContainer
={onPortalContainer}&gt;
&lt;Component {...props} /&gt;
&lt;/IsolatedScope&gt;
);
// React Rootは一度だけ生成（エラー回避）
if (!rootRef.current) { rootRef.current = ReactDOM. createRoot (shadowRoot); }
rootRef.current. render (
&lt;div id=&quot;shadow-root &quot;&gt;
{ /* Shadow DOM内に適用するCSSを読み込む */ }
{styleSheetURLs ?.map((url) =&gt; (
&lt; link rel=&quot;stylesheet &quot; key={url} href={url} /&gt;
))}
{ /* Shadow DOM内にReactコンポーネントを表示する */ }
&lt; div id=&quot;shadow-component-container &quot;&gt;{children}&lt;/ div&gt;
{ /* Portal（Dialog等）描画先 */ }
&lt; div id=&quot;shadow-portal-container &quot;
ref={(portal) =&gt; {if (portal &amp;&amp; onPortalContainer ) onPortalContainer (portal);}}
/&gt;
&lt;/ div&gt;
);
}, [children, styleSheetURLs, onPortalContainer]);
return &lt;div id=&quot;isolated-scope-host &quot; ref={shadowHostRef } /&gt;;
};
}
上記コンポーネントの利用方法
&lt;script type=&quot;module&quot;&gt;
import {
reactShadowDOMRenderer,
Counter
} from &#039;/react/assets/index.js&#039;;
// PHP側の変数をjsonに変換してReact側に直接渡すことができる
const props = &lt;?= json_encode([&#039;value&#039; =&gt; $value]) ?&gt;;
reactShadowDOMRenderer(&#039;root&#039;, Counter, props);
&lt;/script&gt;
&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;


# Page. 25

![Page Image](https://bcdn.docswell.com/page/PEXQQ2LZJX.jpg)

不具合対策済み tailwind.css
長いので、GitHubで公開します
// tailwind.css
@import &quot;tailwindcss&quot; ;
/* Shadow-DOM内で未定義となってしまうため、変数の定義を追加 */
@layer properties {
:host, :host * {
--tw-divide-y-reverse : 0;
--tw-border-style : solid;
--tw-font-weight : initial;
--tw-tracking : initial;
--tw-translate-x : 0;
--tw-translate-y : 0;
--tw-translate-z : 0;
--tw-rotate-x : rotateX (0);
--tw-rotate-y : rotateY (0);
--tw-rotate-z : rotateZ (0);
--tw-skew-x : skewX(0);
--tw-skew-y : skewY(0);
--tw-space-x-reverse : 0;
--tw-gradient-position : initial;
--tw-gradient-from : #0000;
--tw-gradient-via : #0000;
--tw-gradient-to : #0000;
--tw-gradient-stops : initial;
--tw-gradient-via-stops : initial;
--tw-gradient-from-position : 0%;
--tw-gradient-via-position : 50%;
--tw-gradient-to-position : 100%;
--tw-shadow : 0 0 #0000;
--tw-shadow-color : initial;
--tw-inset-shadow : 0 0 #0000;
--tw-inset-shadow-color : initial;
--tw-ring-color : initial;
--tw-ring-shadow : 0 0 #0000;
--tw-inset-ring-color : initial;
--tw-inset-ring-shadow : 0 0 #0000;
--tw-ring-inset : initial;
--tw-ring-offset-width : 0px;
--tw-ring-offset-color : #fff;
--tw-ring-offset-shadow : 0 0 #0000;
--tw-blur : initial;
--tw-brightness : initial;
--tw-contrast : initial;
--tw-grayscale : initial;
--tw-hue-rotate : initial;
--tw-invert : initial;
--tw-opacity : initial;
--tw-saturate : initial;
--tw-sepia : initial;
--tw-drop-shadow : initial;
--tw-duration : initial;
--tw-ease : initial;
}
}


# Page. 26

![Page Image](https://bcdn.docswell.com/page/3EK99MLVED.jpg)

React共存環境の課題 (再掲)
● 最大の課題
○ 15,000行 のスパゲッティ CSS が、Reactに侵入してくる
⇒ Shadow DOM でReact領域を隔離して解決
● Reactを部分導入する構成ならではの課題
○
Reactに初期値をどうやって渡すか問題
⇒ サーバー側変数を JSONとしてpropsに埋め込んで初期化
○
SPAではないReactモジュールをどうやってビルドするか問題
⇒ 巻末の補足資料をご確認ください
※Viteのライブラリモードを利用し、Reactコンポーネントを1つのJS/CSSに
バンドルします


# Page. 27

![Page Image](https://bcdn.docswell.com/page/L73WWY6Q75.jpg)

「SPAにしない React移行」の導入効果
Reactの経験が十分ではない状況にもかかわらず
●
AIコード生成が容易になり、飛躍的な生産性アップ
●
一貫性のある UIデザイン （共通部品、デザインシステム）
●
操作性の向上 （shadcn/ui 利用によるコード量削減）
●
不具合の減少 （スパゲティーコードからの脱却）
この成功体験で「Reactに切り替える方が楽になる」 という納得感を得られました


# Page. 28

![Page Image](https://bcdn.docswell.com/page/87DKK5VWJG.jpg)

React導入前、導入後を比較すると（詳細 )
レガシー環境
React
CSS
15,000行のスパゲッティCSS
特定画面への変更が、別の画面に影響して意図しないバグ
を生む
デザインシステムを考慮した共通UIを使えば、
CSSを書く必要がない
(既存CSSの悪影響も隔離)
JavaScript
js間で暗黙の依存があり、読み込み順を守る必要がある
同一ファイルが複数回ロードされるといった不具合も度々発
生
jsがバンドルされて、１ファイルになったため、依
存性、複数回読み込んでしまうといった問題が原
理的に発生しない
UI作成
HTML(サーバー側テンプレート)、CSS、JavaScriptを必ず
セットで修正
Ajaxで動的にUIを更新する場合、JavaScriptとHTMLの両
方に修正が必要(コードが重複する)
UIとロジックがまとまっているため、修正箇所が
明確
Ajaxによるコードの重複も発生しない
単一ソース内であれば修正可能だが、実際には複数ファイ
ルを修正する必要があり整合性を取るのが大変
修正箇所がはっきりしているため、AIとの相性が
良い
AI利用
「レガシー環境による負債」の解消が進みだした


# Page. 29

![Page Image](https://bcdn.docswell.com/page/VJPKKGVXE8.jpg)

生産性向上への寄与（結果）
●
改善前の工数（全体を100とする）
画面：67、 バックエンド：33
○
●
画面のスタイル調整、jQueryでのロジック作成といった工数が多い
改善後の結果
工数
生産性
フロントエンド
67 ⇒ 22
約3倍
バックエンド
33 ⇒ 26
約1.4倍
全体
100 ⇒ 48
ほぼ２倍
※最近ではCoding Agent や ChatGPT5.5 のおかげで、差が縮まりました・・・


# Page. 30

![Page Image](https://bcdn.docswell.com/page/2EVVV6Z3EQ.jpg)

なぜ、ここまで生産性が上がったのか
●
●
フロントエンド側
○
Shadow DOM によるCSS隔離（スパゲッティ CSS地獄の解消）
○
最新のフロントエンド環境（Tailwind / shadcn / Vite） が使えるように
○
AIが「そのまま使えるコード」を出せる構造（レガシー環境からの独立 ）
バックエンド側
○
Viewロジック減少
○
API化による責務分離
「フロントエンド、バックエンド」を 適切な形に分離した ことで
「AIを活かせる構造に変わった」 ことが最大の要因


# Page. 31

![Page Image](https://bcdn.docswell.com/page/57GLLN4YEL.jpg)

まとめ： SPAにしなくても、 Reactは「武器」になる
今日伝えたかったこと
● SPAではない Reactという選択肢がある
● Reactとレガシーの共存は可能。段階移行ができる
● Shadow DOMはピーキーだけど、強力な味方
「ページ内への Reactの埋め込み 」＋「Shadow DOM 」は
「止められないサービスを動かしながら変えていく 」
ことを可能にする、ひとつの「現実解」である


# Page. 32

![Page Image](https://bcdn.docswell.com/page/4EQYYQG6JP.jpg)

最後に：この手法が向いているシステムとは？
● 止められない「 レガシーシステム 」
● SPA化が「現実的ではない 」
● 安全に「 段階移行 」がしたい
それは「あなたのシステム」かもしれません！！


# Page. 33

![Page Image](https://bcdn.docswell.com/page/KJ4WWN5M71.jpg)

補足：ビルド方法について
Viteのライブラリモード を利用してビルドを行います
●
Vite環境の追加から、ビルドまでの簡易的なチュートリアルです
●
Viteの設定ファイルの変更のみで、JSファイルのビルドと配置が行えます
●
Tailwindも一緒にビルドを行います
●
Rollup.jsを内部で利用していますが、Rollup.jsの追加は不要です
※GitHubでサンプルを公開します
https://github.com/murasuke/isolated-react


# Page. 34

![Page Image](https://bcdn.docswell.com/page/LE1YY5WY7G.jpg)

Vite + React 環境追加
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm install tailwindcss @tailwindcss/vite
●
既存Webアプリ環境のルートに「Vite + React」 環境一式を追加します
ここでは「/frontend」という「サブフォルダ名」にしました（任意）
●
フォルダを分けることで、既存環境との「境」が明確になります


# Page. 35

![Page Image](https://bcdn.docswell.com/page/GEWGG561J2.jpg)

ビルド用のエントリーポイントファイルを作成
画面単位で個別に利用可能とするため、コンポーネントをエクスポートします
●
build.ts
共通部品、各画面をエクスポート
●
vite.conﬁg.ts でエントリーポイントに指定します
// build.ts
export { reactShadowDOMRenderer } from &#039;./reactRenderer&#039;;
export { default as IsolatedScope } from &#039;./IsolatedScope&#039;;
import &quot;./tailwind.css&quot;;
// 本番用エントリポイント
// Node.js アプリ側から import できるように、埋め込み用の部品をまとめて export する。
export { RandomApp } from &#039;./RandomApp&#039;;


# Page. 36

![Page Image](https://bcdn.docswell.com/page/47ZLLNYXJ3.jpg)

Vite ライブラリモード設定（ vite.conﬁg.ts）
●
Tailwindプラグインを追加
import { defineConfig } from &#039;vite&#039;
import react from &#039;@vitejs/plugin-react&#039;
import tailwindcss from &quot;@tailwindcss/vite&quot;
●
NODE_ENV 未定義エラー回避のため変数
を追加
●
lib: { … } を追加しライブラリモードを有効に
する(単一ファイルに格納)
●
build.ts をビルドのエントリーポイントに指
定
●
出力ファイル名をindex.jsに固定化
●
outDir: で既存Webアプリ側へ出力する
// https://vite.dev/config/
export default defineConfig ({
plugins: [
react(),
tailwindcss (),
],
base: &quot;/react/&quot; ,
define: { // React 実行時エラー回避のため
&quot;process.env.NODE_ENV&quot; : JSON.stringify (&quot;production&quot; ),
},
server: {
port: 5173,
proxy: { &quot;/api&quot;: &quot;http://localhost:3000&quot; , },
},
build: {
outDir: &quot;../public/react&quot; ,
emptyOutDir: true,
lib: {
entry: &quot;src/build.ts&quot; ,
formats: [&quot;es&quot;],
fileName : () =&gt; &quot;assets/index.js&quot; ,
},
rollupOptions: {
output: {
assetFileNames: &quot;assets/[name][extname]&quot; ,
},
},
},
})


# Page. 37

![Page Image](https://bcdn.docswell.com/page/YJ6WW8DPJV.jpg)

ビルド＆動作確認用に簡単なコンポーネントを作成
よくある「カウンター」コンポーネント（初期値指定あり）
// src/Counter.tsx
import { useState } from &#039;react&#039;;
interface CounterProps {
initialCount?: number;
}
export function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
const increment = () =&gt; {
setCount(count + 1);
};
return (
&lt;div className=&quot;m-4 p-4 border rounded w-fit&quot;&gt;
&lt;p className=&quot;mb-2&quot;&gt;Count: {count}&lt;/p&gt;
&lt;button className=&#039;px-4 py-2 bg-blue-500 text-white rounded&#039;
onClick={increment}&gt;Increment&lt;/button&gt;
&lt;/div&gt;
);
};


# Page. 38

![Page Image](https://bcdn.docswell.com/page/GJ5MM93PJ4.jpg)

ビルド後の動作確認
Webページの一部にReactコンポーネントを表示する
ビルド後のJSファイルを読み込み、
Shadow DOM 内でReactが表示されているこ
とを確認します
●
Counterが表示される
●
外部style(color: red)が
React領域には適用されない
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;meta charset=&quot;UTF-8&quot; /&gt;
&lt;style&gt;
color: red;
&lt;/style&gt;
&lt;script type=&quot;module&quot;&gt;
import {
reactShadowDOMRenderer,
Counter
} from &#039;/react/assets/index.js&#039;;
const props = {&quot;initialCount&quot;: 3}; // テスト用に固定データをセット
reactShadowDOMRenderer (&#039;root&#039;, Counter , props);
&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;Webページの一部にReactコンポーネントを表示する&lt;/
h2&gt;
&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;


# Page. 39

![Page Image](https://bcdn.docswell.com/page/LE3WWY66E5.jpg)

参考：Rollupビルド設定（ rollup.conﬁg.js）
Rollup.js でもビルドを行うことができます。(微
調整が可能)
●
build.ts をビルドのエントリーポイントとし
て指定
●
NODE_ENV 未定義エラーを回避するた
め、ソースの置換を行う
●
本番(BUILD_MODE=’release’)では
minify、開発時はsourcemapを生成す
るように分岐
// rollup.config.js (importは省略)
const isRelease = process.env. BUILD_MODE === &#039;release&#039; ;
export default {
input: &#039;src/build.tsx&#039; , // ビルドのエントリーポイント
output: {
file: &#039;dist/bundle-react.js&#039; , // 出力ファイル名
format: &#039;esm&#039;,
sourcemap : !isRelease, // デバッグビルド時はsourcemapを生成する
},
plugins: [
resolve(),
commonjs (),
replace({ // ビルド時に&#039;NODE_ENV&#039;が取れないので、ソースの置換を行う
&#039;process.env.NODE_ENV&#039; : JSON.stringify (NODE_ENV ),
preventAssignment : true,
}),
typescript ({
tsconfig : &#039;tsconfig.react.json&#039; , // TypeScriptビルド設定
}),
postcss ({ // ここで PostCSS に Tailwind v4 用プラグインを渡す
plugins: [tailwind ()],
extract: &#039;bundle-react.css&#039; , // 出力ファイル名(jsと同じディレクトリ)
minimize : isRelease,
sourceMap : !isRelease,
}),
isRelease &amp;&amp; terser(), // リリースビルド時のみ minify
].filter(Boolean), // `falsy` な要素を削除(Booleanは述語関数として利用)
onwarn(warning, warn) {
// ワーニング&quot;Module level directives cause errors when bundled&quot;抑制のための処理
if (warning.code === &quot;MODULE_LEVEL_DIRECTIVE&quot; ) { return; }
warn(warning);
},
};


