はじめに
Reactのコンポーネントを従来型Webページの一部に組み込むにはどうすればいいのか?と思い調べてみましたが、意外にも具体的な手順が見つかりませんでした
そこで、コンポーネントのビルド、ページ内の部品として表示、コンポーネント外部とのやり取りの3点に絞って手順をまとめてみます
- Reactコンポーネントを外部から利用できるようにビルドする手順
- ReactのコンポーネントをWebページの一部として表示する手順
- Webページ側とReactコンポーネント間でやり取りを行う(初期値設定、相互の呼び出し)
概要
create-react-appを使ってひな形を生成し、おなじみCounter
コンポーネントを作ってから、外部で利用できるようにしていきます
- Reactコンポーネントを外部から利用できるようにビルド(rollup.jsを利用)する
- 従来型Webアプリではモジュール形式のJSを利用してないことが多いので、UMD形式(グローバル変数経由でReactコンポーネントを公開)でビルドする
- 埋め込むコンポーネントはReactでおなじみの
Counter
コンポーネントを利用
- ReactのコンポーネントをWebページの一部として表示する
- Webページ側とReactコンポーネント間でやり取りを行う
- Reactコンポーネントへの初期値設定
- Reactコンポーネント側のイベント処理から、Webページ側の処理を呼び出す
- Webページ側のイベント処理から、React内部の処理を呼び出す
Counter
コンポーネントを部品として表示するコード(のイメージ)
-
MyBundle
はコンポーネントを公開するためのグローバル変数(UMD形式でビルドする際に、指定した変数名) -
ReactDOMClient.createRoot()
でroot
を作成し、コンポーネントをrenderします
<body>
<h2>Webページの一部にReactコンポーネントを表示する</h2>
<div id="root"></div>
</body>
<script src="/dist/lib.umd.js"></script>
<script>
const {ReactDOMClient, React, Counter} = MyBundle;
const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(container);
root.render(React.createElement(Counter));
</script>
ESModule形式でビルドした場合はこのようになります
<script type="module">
import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(container);
root.render(React.createElement(Counter));
</script>
準備:Counter
コンポーネントを作成
- create-react-appでReactのひな形を作成
$ npx create-react-app react-with-conventional-webapp --template typescript
$ cd react-with-conventional-webapp
-
styled-components
をインストール(Reactコンポーネントを目立だたせるため、borderを表示する)
$ npm i styled-components@5.3.10
$ npm i -D @types/styled-components
- 下記のエラーが出た場合は
npm i styled-components@5.3.10
でインストール可能 (StackOverflowの解説)
$ npm i styled-components
npm ERR! Cannot read properties of null (reading 'edgesOut')
-
Counter
コンポーネント(ボタンを押すとカウントを加算)を追加
$ touch ./src/Counter.tsx
Counter.tsx
import { FC, useState } from 'react';
import styled from 'styled-components';
const Content = styled.div`
border: solid 1px black;
width: fit-content;
padding: 4px;
`;
type propType = {
initVal?: number;
};
const Counter: FC<propType> = ({ initVal = 0 }) => {
const [count, setCount] = useState(initVal);
const handleClick = () => setCount((n) => n + 1);
return (
<Content>
<div>Count: {count}</div>
<button onClick={handleClick}>Increment</button>
</Content>
);
};
export default Counter;
動作確認
-
App.tsx
ファイルを書き換えてからnpm run start
で実行する
import './App.css';
import Counter from './Counter';
function App() {
return (
<div className="App">
Counterコンポーネント動作確認
<Counter initVal={0} />
</div>
);
}
export default App;
このような画面が表示されればOK
①Reactコンポーネントを外部から利用できるようにビルド(rollup.jsを利用)する
ビルドを行うための手順
- rollup.jsの導入
- ビルド用スクリプト
rollup.config.js
を作成 -
rollup.jsでトランスパイルを行うため、
tsconfig.rollup.json
を追加 -
package.json
に出力ファイル名を追加(rollup.config.js
で利用) - 公開するコンポーネントをエントリーポイントファイル(
src/lib.ts
)でexportする -
package.json
のscripts
にビルド用のコマンドbuild-lib
を追加して動作確認を行う
rollup.jsの導入
ライブラリとしてパッケージングするため、rollup.jsと必要なプラグインを導入します
- rollup.jsは複数のモジュールやファイルを1つのファイルにまとめてくれる、軽量で高速なバンドルツールです
npm i -D rollup rollup-plugin-delete rollup-plugin-peer-deps-external rollup-plugin-typescript2 @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace
ビルド用スクリプトrollup.config.js
の作成
続いて、ビルド用のスクリプトを作成します。rollup.config.js
をプロジェクトのルートに作成し、下記の内容を記載します
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
import typescript from 'rollup-plugin-typescript2';
import del from 'rollup-plugin-delete';
const packageJson = require('./package.json');
// eslint-disable-next-line import/no-anonymous-default-export
export default {
input: 'src/lib.ts',
output: [
{
// esmodule
file: packageJson.module,
format: 'esm',
sourcemap: true,
},
{ // UMD形式
file: packageJson.umd,
format: 'umd',
name: 'MyBundle',
sourcemap: true,
},
],
plugins: [
del({ targets: 'dist/*' }),
peerDepsExternal(),
resolve(),
commonjs(),
replace({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
preventAssignment: true,
}),
typescript({
tsconfig: 'tsconfig.rollup.json',
useTsconfigDeclarationDir: true,
}),
],
};
-
input: 'src/lib.ts'
を起点にimportされているファイルを読み込み、バンドルします(1つのファイルにまとめる)。公開したいコンポーネントをexportするファイルです -
ES Modules
形式、UMD
形式に分けて2種類のファイル出力します -
file:~
は出力するファイル名です。package.json
で定義した値を利用します(この後追加)
rollup.jsで利用するプラグインについて
-
peerDepsExternal
package.json
に記載されたpeerDependenciesパッケージをバンドル対象から除外して、バンドルサイズを削減する -
resolve
インポートするモジュールの依存関係の解決とファイルパスの特定を行う
-
commonjs
require()を解析してCommonJS形式の依存関係を特定する
-
replace
ビルド時に置換処理を行う。実行時にnodeの環境変数を参照する箇所がエラーとなるため、ビルド時に置換する
-
typescript
トランスパイルを行う。設定ファイルは
tsconfig.rollup.json
を利用する。また型定義ファイル(d.ts)を出力するため、useTsconfigDeclarationDir: true
を指定する。出力先はtsconfigファイルのdeclarationDir
rollup.jsでトランスパイルを行うため、tsconfig.rollup.json
を追加
rollup.js用トランスパイル設定ファイルtsconfig.rollup.json
をプロジェクトルートに作成します
{
"extends": "./tsconfig",
"compilerOptions": {
"outDir": "dist",
"declaration": true,
"declarationDir": "dist",
},
"exclude": [
"node_modules",
"dist",
"build",
]
}
-
"extends": "./tsconfig",
create-react-appで作成された
tsconfig.json
の設定を継承します -
"outDir": "dist",
出力先の指定
-
"declaration": true,
、"declarationDir": "dist"
型定義ファイルの出力を有効にして、出力先を
dist
にします
package.json
に出力ファイル名を追加(rollup.config.js
で利用)
rollup.jsで出力するファイル名の設定を追加します(ESModule用と、UMD用の2つ)
{
"name": "react-with-conventional-webapp",
"version": "0.1.0",
"private": true,
+ "module": "dist/lib.esm.js",
+ "umd": "dist/lib.umd.js",
公開するコンポーネントをエントリーポイントファイル(src/lib.ts
)でexportする
公開するコンポーネント(関数)をexportします
Counter
だけではなく、React
自体も一緒にexportしてパッケージングします(別途読み込み不要になる)
export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';
package.json
のscripts
にビルド用のコマンドを追加して動作確認
パッケージビルド用のコマンドを追記します
"scripts": {
+ "build-lib": "rollup -c"
},
ビルドエラーが発生しないことを確認します
- エラーがなければ
dist/lib.esm.js, dist/lib.umd.js
の2ファイルが作成されます
$ npm run build-lib
> react-with-conventional-webapp@0.1.0 build-lib
> rollup -c
src/lib.ts → dist/lib.esm.js, dist/lib.umd.js...
created dist/lib.esm.js, dist/lib.umd.js in 4.1s
②ReactのコンポーネントをWebページの一部として表示する
動作確認のためhtmlファイルを作成します。webroot
フォルダをルート直下に作成しその中に保存していきます
$ mkdir webroot
$ touch webroot/test1.html
test1.html
に下記内容を書き込みます
-
<div id="root"></div>
の部分に、Reactコンポーネント(Counter
)を描画
<!DOCTYPE html>
<html>
<body>
<h2>Webページの一部にReactコンポーネントを表示する</h2>
<div id="root"></div>
</body>
<script src="/dist/lib.umd.js"></script>
<script>
const {ReactDOMClient, React, Counter} = MyBundle;
const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(container);
root.render(React.createElement(Counter));
</script>
</html>
動作確認のためにWebサーバーを起動します
$ npx http-server .
http://localhost:8080/webroot/test1.html
を開きます。htmlの一部としてReactコンポーネントが表示されることを確認します
- ESModule形式(lib.esm.js)を利用する場合は以下の記述になります
test1_esm.html
<!DOCTYPE html>
<html>
<body>
<h2>Webページの一部にReactコンポーネントを表示する</h2>
<div id="root"></div>
</body>
<script type="module">
import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(container);
root.render(React.createElement(Counter));
</script>
</html>
③Webページ側とReactコンポーネント間でやり取りを行う
Reactコンポーネントへの初期値設定
Counterコンポーネントは引数(属性)で初期値をセットできるようになっています
type propType = {
initVal?: number;
};
const Counter: FC<propType> = ({ initVal = 0 }) => {
React.createElement()
の引数に、initVal
を追加することで初期値を設定することができます
- PHPなどサーバ側から初期値をセットして表示することが可能です
test2.html
<!DOCTYPE html>
<html>
<body>
<h2>Webページの一部にReactコンポーネントを表示する</h2>
<div id="root"></div>
</body>
<script type="module">
import {ReactDOMClient, React, Counter} from '/dist/lib.esm.js';
const container = document.getElementById('root');
const root = ReactDOMClient.createRoot(container);
root.render(React.createElement(Counter,{initVal:12}));
</script>
</html>
Reactコンポーネント側のイベント処理から、Webページ側の処理を呼び出す
propsにcallback関数を追加して、React側から呼び出します(countを渡す)
CallbackCounter.tsx
import { FC, useState } from 'react';
import styled from 'styled-components';
const Content = styled.div`
border: solid 1px black;
width: fit-content;
padding: 4px;
`;
type propType = {
initVal?: number;
callback: (num: number) => number;
};
const CallbackCounter: FC<propType> = ({ initVal = 0, callback }) => {
const [count, setCount] = useState(initVal);
const handleClick = () => {
setCount((n) => n + 1);
if (callback) {
callback(count + 1);
}
};
return (
<Content>
<div>Count: {count}</div>
<button onClick={handleClick}>Increment</button>
</Content>
);
};
export default CallbackCounter;
CallbackCounterをexportします
lib.ts
export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';
+ export { default as CallbackCounter } from './CallbackCounter';
html側でコンポーネントにcallback関数
を渡します。callback関数は、引数経由で受け取ったcountの値を表示します
test3.html
<!DOCTYPE html>
<html>
<body>
<h2>React側のイベントをWebページ側に通知する</h2>
<p>
callback:<span id="callback">
</p>
<div id="root"></div>
</body>
<script src="/dist/lib.umd.js"></script>
<script>
const {ReactDOMClient, React, CallbackCounter} = MyBundle;
const $ = (selectors) => document.querySelector(selectors);
const root = ReactDOMClient.createRoot($('#root'));
root.render(
React.createElement(CallbackCounter,{
callback: (val)=> $('#callback').innerText = `${val}`}
)
);
</script>
</html>
ボタンをクリックすると、callback関数が呼び出されてcallback:
の後ろのカウントが更新されます
Webページ側のイベント処理から、React内部の処理を呼び出す
Webページ側から、Reactの処理を呼び出すためにカスタムイベントを利用します
1:カスタムイベントを受信するコンポーネントを作成します
-
ButtonClick
という名前のカスタムイベントを受信する - イベントのデータ(日時)を受け取り、yyyyMMdd HHmmss形式にフォーマットして表示する
EventReceiver.tsx
import { useState, useEffect } from 'react';
import styled from 'styled-components';
// カスタムイベントの型設定
declare global {
interface DocumentEventMap {
ButtonClick: CustomEvent<Date>;
}
}
const Content = styled.div`
border: solid 1px black;
width: fit-content;
padding: 4px;
`;
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('ja-jp', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).format(date);
};
const EventReceiver = () => {
const [log, setLog] = useState('');
useEffect(() => {
document.addEventListener('ButtonClick', appendEventLog);
return () => {
document.removeEventListener('ButtonClick', appendEventLog);
};
}, []);
function appendEventLog(data: CustomEvent<Date>) {
setLog((value) => value + formatDate(data.detail) + '\r\n');
}
return (
<Content>
<p>React Component</p>
<textarea value={log} style={{ height: '10em' }}></textarea>
</Content>
);
};
export default EventReceiver;
EventReceiverをexportします
lib.ts
export { default as React } from 'react';
export { default as ReactDOM } from 'react-dom';
export { default as ReactDOMClient } from 'react-dom/client';
// Component
export { default as Counter } from './Counter';
export { default as CallbackCounter } from './CallbackCounter';
export { default as EventReceiver } from './EventReceiver';
2:Webページ側からカスタムイベントを配信する
ボタンのクリック時に、イベントを発生させて現在時刻を渡します
test4.html
<!DOCTYPE html>
<html>
<body>
<h2>Webページ側のイベントから、React側の処理を呼び出す</h2>
<p>
callback:<span id="callback">
</p>
<div id="root"></div>
</body>
<script src="/dist/lib.umd.js"></script>
<script>
const {ReactDOMClient, React, EventReceiver} = MyBundle;
const $ = (selectors) => document.querySelector(selectors);
$('#outerButton').addEventListener('click', () => {
// イベントを配信
const event = new CustomEvent('ButtonClick', {detail: new Date()});
document.dispatchEvent(event);
});
const root = ReactDOMClient.createRoot($('#root'));
root.render(
React.createElement(EventReceiver)
);
</script>
</html>
ボタンをクリックすると、カスタムイベント経由でReactに通知できました