Reactを使ってアプリケーションを構築したい時にViteやcreate-react-appを利用することでコマンド一つで開発環境を構築することができます。本文書ではそれらのツールを利用することなしで一からReactの開発環境を構築してみたいという人向けの記事です。

開発ツールを利用することなしでReactの開発環境を構築することで開発環境を構築するためにはどのようなツールがなぜ必要なのかということを理解することができます。

本文書では下記のツールを利用して動作確認を行います。

  • webpack + Babel
  • Parcel
  • Rollup
  • esbuild
  • Snowpack

ここからの手順は手元の環境にNode.jsがインストールされていることを前提に進めます。

通常JavaScriptはブラウザのみで動作しますがNode.jsを利用することでブラウザ外の環境でもJavaScriptを動作させることができます。Node.jsをインストールするとnpm(Node Package Manager)というJavaScriptのパッケージ管理マネジャーがインストールされます。JavaScriptのパッケージ管理にnpm、OS上でJavaScriptを動作させるためにNode.jsを利用します。

Webpack+Babel

プロジェクトフォルダの作成

任意のフォルダを作成します。ここではcreate-react-appという名前にしています。create-react-appフォルダに移動してnpm init -yコマンドを実行してpackage.jsonファイルを作成します。


% npm init -y
Wrote to /Users/mac/Desktop/create-react-app/package.json:

{
  "name": "create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

npm initコマンドにオプションの-yをつけない場合は対話式でpackage.jsonファイルを作成します。オプションの-y(es)を利用することで対話をスキップしてデフォルト値のpackage.jsonファイルが作成されます。

package.jsonファイルはインストールするJavaScriptのパッケージの依存関係、コマンドのエイリアス(コマンド名に別名をつけて簡単に実行できるようにする)を設定するファイルです。

Reactのインストール

reactとreact-domのパッケージをインストールします。


 % npm install react react-dom

reactはUI(ユーザインターフェイス)ライブラリでブラウザ上に表示させるUIを作成するために利用します。reactを利用した作成したUIはReact Elementで構成されています。React Elementをブラウザ上に表示させるためにreact-domを利用します。react-domが持つrender関数を利用してどのDOMに対してReact Elementを挿入するか指定します。

DOMはDocument Object Modalの略でHTMLを元に作成されるHTML要素で構成されたツリー構造をしています。 DOMを構成するHTML要素にアクセスすることでHTML要素を操作することができます。

インストール後にpackage.jsonファイルを確認するとdependenciesにインストールしたreact, react-domのパッケージを確認することができます。


{
  "name": "create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Webpackのインストール

モジュールバンドラーのwebpackのインストールを行います。ここでいうモジュールというものはファイルに対応します。バンドラーは束ねるという意味を持っているので複数のファイルを1つのファイルにまとめるためのツールです。バンドル以外にも機能を持っており、Reactの場合ではJSXをJavaScriptへ変換する際にwebpackにbabel Loaderを追加します。

webpackのコマンドラインを利用するためにwebpack-cliも一緒にインストールします。webpackは開発環境のみに利用するツールなので–save-dev(-D)を利用してインストールします。


 % npm install --save-dev webpack webpack-cli

インストール後にpackage.jsonファイルを確認するとdevDepenciesのプロパティにインストールしたwebpackの依存情報が表示されていることが確認できます。


{
  "name": "create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  }
}

最小の構成でHello World

react, react-dom, webpackのみをインストールした最小構成でReactを利用してブラウザ上に”Hello World”を表示させてみましょう。npx webpackコマンドを実行することでビルドを行うことができますがsrcフォルダがないためエラーになります。プロジェクトフォルダ直下にsrcフォルダを作成してその下にindex.jsファイルを作成します。index.jsファイルには以下のコードを記述します。


import React from 'react';
import ReactDOM from 'react-dom/client';

const App = React.createElement('h1', null, 'Hello World');

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App);

ReactのcreateElementメソッドを利用して文字列”Hello World”を持つh1タグのReact Elementを作成しています。作成したReact Elementをdocument.getElementById(‘root’)で取得したdiv要素の中に挿入することでブラウザ上に表示させています。

JSXを利用して記述していないのはJSXはJavaScriptを拡張しているためブラウザ上で表示させるためにはJavaScriptへの変換が必要になるためです。JSXは変換を行うとcreateElementメソッドを利用した構文に変換されるためここではJSX変換後のcreateElementメソッドを利用しています。通常はReactで開発する際はJSXを利用するためcreateElementを直接利用する機会はほとんどありません。JSXについては後ほど説明します。

webpackを利用してビルドするためのsrc/index.jsファイルが作成できたのでnpx webpackコマンドを実行します。WARNINGが表示されますが無視してください。


% npx webpack
asset main.js 137 KiB [compared for emit] [minimized] (name: main) 1 related asset
modules by path ./node_modules/react-dom/ 131 KiB
  ./node_modules/react-dom/client.js 619 bytes [built] [code generated]
  ./node_modules/react-dom/index.js 1.33 KiB [built] [code generated]
  ./node_modules/react-dom/cjs/react-dom.production.min.js 129 KiB [built] [code generated]
modules by path ./node_modules/react/ 6.94 KiB
  ./node_modules/react/index.js 190 bytes [built] [code generated]
  ./node_modules/react/cjs/react.production.min.js 6.75 KiB [built] [code generated]
modules by path ./node_modules/scheduler/ 4.33 KiB
  ./node_modules/scheduler/index.js 198 bytes [built] [code generated]
  ./node_modules/scheduler/cjs/scheduler.production.min.js 4.14 KiB [built] [code generated]
./src/index.js 216 bytes [built] [code generated]

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

実行が完了するとプロジェクトフォルダ直下にdistフォルダが作成されその下にmain.jsファイルが作成できていることが確認できます。

ビルドによりブラウザで処理できるmain.jsファイルが作成できたのでプロジェクトフォルダ直下にindex.htmlファイルを作成しidにrootを持つdiv要素を追加し、webpackのビルドで作成したmain.jsファイルをscriptタグで指定します。scriptタグにdeferをつけることでDOMが構築された後にmain.jsファイルが実行されるようにしています。


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <script defer src="./dist/main.js"></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

index.htmlファイルをブラウザを使って表示するとブラウザ上には”Hello World”が表示されました。

Hello Worldの表示
Hello Worldの表示

ここまでの設定でReactを利用して”Hello World”をブラウザ上に表示することができました。

Appファイルを別ファイルにするためsrcフォルダにApp.jsファイルを作成します。


import React from 'react';
const App = () => {
  return React.createElement('h1', null, 'Hello World');
};

export default App;

index.jsファイルで作成したAppをimportしています。importしたAppは関数なのでrender関数ではApp()としています。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App());

先ほどまでと同様に画面には”Hello Worlld”が表示されます。”Helllo World”をブラウザ上に表示することができたのでuseStateやuseEffectなどのHookを利用しようとするとここまでの設定では下記のエラーが発生して動作しません。


import React, { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return React.createElement('h1', null, 'Hello World');
};

export default App;
エラー画面
エラー画面

render関数の引数をApp()からReact.createElementに変更します。


import ReactDOM from 'react-dom/client';
import React from 'react';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(React.createElement(App, null, null));

ブラウザ上には”Hello World”が表示され、コンソールにはuseEffectで設定した’test’の文字列が表示されます。JSXではなくcreateElementを利用しましたが、react, react-domとwebpackを利用するだけでReactでアプリケーションを構築することができることがわかりました。

Webpackの設定

npx webpackコマンドを利用してビルドを行なっていましたが開発を効率的に行うことと後ほど利用するbabel設定のためにwebpackの設定ファイルを作成します。ここまで確認した通りnpx webpackコマンドを実行することでビルドが行えるのでwebpackの設定ファイルは必須ではありません。

プロジェクトフォルダの直下にwebpack.config.jsファイルを作成して以下を記述します。Node.js上でJavaScriptを動かしているのでpathモジュールを利用することができます。pathモジュールはパスの設定に利用します。entryにはビルド前のファイル、outputにはビルド後に作成されるファイルと場所を設定しています。設定はwebpack.config.jsを作成前と同じにしています。


const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

wepack-dev-serverのインストール

開発サーバを利用して開発が行えるようにwebpack-dev-serverのインストールを行います。HMR(Hot Module Replcement)などの機能を持っているのでファイルの更新を行うと自動で検知してくれます。


 % npm install webpack-dev-server --save-dev

開発サーバの設定はwebpack.config.jsファイルで行うことができ下記では公開フォルダをpublicにしています。publicフォルダの下にindex.htmlファイルを保存することで開発サーバが起動するとindex.htmlにアクセスが行われindex.htmlファイルの内容が表示されます。


const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
  },
};

publicフォルダを作成して先ほどまで利用していた作成済みのindex.htmlファイルを移動してください。index.htmlにはscriptタグを追加していましましたが削除してください。これからインストールするhtml-webpack-pluginを利用することで自動でscriptタグが追加されます。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

html-webpack-plugin

html-webpack-pluginをインストールすることでindex.htmlファイルに自動でscriptタグを追加してくれます。さらにproduction modeでビルドするとdistフォルダにindex.htmlファイルも作成してくれます。


 % npm install --save-dev html-webpack-plugin

設定はwebpack.config.jsで行います。html-webpack-pluginの設定はpluginsのHtmlWebpackPluginの引数にscriptタグを追加するファイルをtemplateとして指定しています。Productionビルドした際はこのファイルを元にファイルが作成されます。開発環境でもindex.htmlにscriptタグが追加されます。


const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'public', 'index.html'),
    }),
  ],
};

scriptsの追加

package.jsonファイルのscriptsに開発サーバを起動するためのコマンドを追加します。webpack-dev-serverコマンドのオプションに–modeでdevelopmentを設定することで開発モードでwebpackがビルドするように設定を行なっています。–openオプションは実行するとブラウザのタブが自動で開いてページが表示されます。自動でHMRは行われるのでオプションの指定は必要ありません。


{
  "name": "create-react-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --mode development --open"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@babel/core": "^7.21.0",
    "@babel/preset-react": "^7.18.6",
    "babel-loader": "^9.1.2",
    "html-webpack-plugin": "^5.5.0",
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1"
  }
}

scriptsにstartを追加したのでnpm startコマンドを実行することができます。実行するとブラウザのタブが自動で開いて”Hello World”と表示されます。ポート番号が8080であることも確認できます。

開発サーバの起動
開発サーバの起動

ブラウザのソースコードを見るとheadタグの閉じタグの前にscriptタグが追加されていることがわかります。


<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>React App</title>
<script defer src="main.js"></script></head>
<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>
</html>

Production用のscriptもpackage.jsonファイルに追加します。


"scripts": {
  "start": "webpack-dev-server --mode development --open",
  "build": "webpack --mode production"
},

npm run buildコマンドを実行するとビルドが実行されdistフォルダにはmain.jsだけではなくindex.htmlファイルが作成されます。

Babelの利用

ここまでの設定でReact.createElementを利用することでReactアプリケーションを構築できることがわかりました。createElementではなくJSXの形でコードを記述できるようにrender関数の引数をコンポーネントタグ<App />に変更を行います。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

npm startコマンドを実行すると変更した<App />に関してエラーが発生します。このタグを処理するためにloaderが必要であることがメッセージからわかります。


You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| const root = ReactDOM.createRoot(document.getElementById('root'));
| 
> root.render(<App />);
| 

コンポーネントタグを変換するためのloaderにBabelを利用します。Babelを利用することでJSXのタグを理解してJSXをJavaScriptに変換することができます。JSXからJavaScriptへの変換にはBabelだけではなくTypeScriptでも行うことができます。

通常Reactを開発する際はJSXを利用します。JSXはHTMLと似た書式でJavaScriptのコードも一緒に記述することができます。JSXはHTMLでもJavaScriptでもなくReact用にJavaScriptを拡張したものなのでブラウザ上に表示させるためにはJavaScriptへの変換が必要になります。JSXはReact限定されたものではなく他のフレームワークでも利用できるものがあります。

Babelのインストール

Babelを利用するためにはbabel-loaderと@babel/coreをインストールします。


 % npm install --save-dev babel-loader @babel/core

JSXをJavaScriptに変換するためのプラグインには@babel/plugin-transform-react-jsxをインストールする必要があります。Babelには@babel/plugin-transform-react-jsx、@babel/plugin-syntax-jsx、@babel/plugin-transform-react-display-nameの3つのプラグインを含むプリセットの@babel/presets-reactというものが準備されているのでインストールを行います。


 % npm install --save-dev @babel/preset-react

webpack.config.jsファイルにbabelを利用するための設定を行います。moduleのrulesの中で設定を行っています。testでは変換の対象となるファイルを指定しています。拡張子にjsを付くものを対象とし、excludeはloaderの対象外となるnode_modulesフォルダを指定しています。node_modulesフォルダのjsファイルは変換の対象外となります。loaderにはbabel-loaderを指定しています。


const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
  devServer: {
    static: {
      directory: path.join(__dirname, 'public'),
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.join(__dirname, 'public', 'index.html'),
    }),
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
    ],
  },
};

babel用の設定ファイル.babelrcをプロジェクトフォルダ直下に作成します。インストールした@babel/preset-reactをpresetsに設定します。


{
  "presets": ["@babel/preset-react"]
}

設定は完了です。

npm startコマンドを実行するとブラウザ画面にはHello Worldが表示されます。bable loaderを利用することで<App />タグを処理することができるようになりました。

App.jsファイルではcreateElementを利用してコードを記述していましたがJSXに書き換えます。


import { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);
  return <h1>Hello World!</h1>;
};

export default App;

App.jsを書き換えてもブラウザ上にはHello Worldが表示され、ブラウザのコンソールにはuseEffectで実行される”test”の文字列が表示されます。

.babelrcではpreset-reactを設定していましたがpluginの@babel/plugin-transform-react-jsxを指定するたけでも同じように動作します。


{
  "plugins": ["@babel/plugin-transform-react-jsx"]
}

ここまで読み進めてもらえれば開発環境を構築できただけではなくReactで開発環境を構築するためにはどのツールが何のために必要になるのかということまで理解できたのではないでしょうか。

Parcel

次はParcelというビルドツールを利用してReactの環境を構築してみましょう。あっという間にwebpack + babelと同じ環境を構築することができます。

プロジェクトの作成

create-react-app-parcelという名前のフォルダを作成します。フォルダ名は任意です。

create-react-app-parcelに移動してnpm init -yコマンドを実行してpackage.jsonファイルを作成してください。


% npm init -y
Wrote to /Users/mac/Desktop/create-react-app-parcel/package.json:

{
  "name": "create-react-app-parcel",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Reactのインストール

reactとreact-domのパッケージをインストールします。


 % npm install react react-dom

インストール後にpackage.jsonファイルを確認するとdependenciesにインストールしたreact, react-domのパッケージを確認することができます。


{
  "name": "create-react-app-parcel",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Parcelのインストール

ビルドツールのparcelのインストールを行います。


% npm install --save-dev parcel

インストール直後のpackage.jsonファイルは下記の通りです。parcelはdevDependenciesに追加されていることが確認できます。


{
  "name": "create-react-app-parcel",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "parcel": "^2.8.3",
    "process": "^0.11.10"
  }
}

Hello Worldの表示

webpackの時と同様にsrcフォルダを作成します。srcフォルダにindex.htmlとindex.jsファイルを作成してください。

index.htmlには以下のコードを記述します。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <script type="module" src="index.js" defer></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

JSXではなくReact.createElementを利用してHello Worldを表示させます。


import React from 'react';

import ReactDOM from 'react-dom/client';

const App = React.createElement('h1', null, 'Hello World');

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App);

設定は以上で完了です。開発サーバを起動するためにnpx parcel src/index.htmlコマンドを実行します。


 % npx parcel src/index.html    
Server running at http://localhost:1234
✨ Built in 300ms
–openのオプションをつけて実行するとブラウザのタブが自動で開きページが表示されます。

localhostのポート1234で起動していることがわかります。ブラウザからアクセスすると”Hello World”が表示されていることが確認できます。

Hello Worldの表示
Hello Worldの表示

npx parcel src/index.htmlコマンドを実行するとプロジェクトフォルダ直下にdistフォルダが作成され、その中にindex.XXXX.jsファイルindex.XXXX.js.mapとindex.htmlファイルが作成されていることがわかります。

ブラウザ上に表示されているindex.htmlファイルはdistファイル内のファイルとなります。scriptタグで設定したindex.jsファイルがdistフォルダ内に作成されているindex.XXXXX.jsファイルに変わっていることが確認できます。

ブラウザでindex.htmlファイルのソースを確認
ブラウザでindex.htmlファイルのソースを確認

JSXを利用した場合

React.createElementではなくJSXで記述するためApp.jsファイルを作成して下記のコードを記述します。


import { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World</h1>;
};

export default App;

作成したApp.jsファイルをindex.jsファイルからimportしてrender関数の引数に設定します。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

ブラウザを確認すると”Hello World”が表示されブラウザのコンソールには”test”の文字列が表示されていることが確認できます。

JSXを利用した場合
JSXを利用した場合

webpack + babelから読んでいる人であればJSXからHTMLの変換は?という疑問がでてきているのではないでしょうか。

Parcelのドキュメントを確認すると”Parcel supports JSX automatically when it detects you are using React.”と記述されています。Parcelが自動でJSXからの変換を行ってくれます。

HMR(Hot Module Replacement)にも対応しているのでApp.jsを更新するとBuildが自動で行われるので更新した内容がブラウザ上に反映されます。

Rollup

次はJavaScriptのモジュールバンドラーであるRollupを利用してReactの環境を構築してみましょう。webpackの時と同様にJSXの変換にbabelを利用します。

プロジェクトの作成

create-react-app-rollupという名前のフォルダを作成します。フォルダ名は任意です。

create-react-app-rolluplに移動してnpm init -yコマンドを実行してpackage.jsonファイルを作成してください。


% npm init -y
Wrote to /Users/mac/Desktop/create-react-app-rollup/package.json:

{
  "name": "create-react-app-rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Reactのインストール

reactとreact-domのパッケージをインストールします。


 % npm install react react-dom

インストール後にpackage.jsonファイルを確認するとdependenciesにインストールしたreact, react-domのパッケージを確認することができます。


{
  "name": "create-react-app-rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Rollupのインストール

モジュールバンドラーのRollupのインストールを行います。


 % npm install --save-dev rollup

インストール直後のpackage.jsonファイルは下記の通りです。parcelはdevDependenciesに追加されていることが確認できます。


{
  "name": "create-react-app-rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "rollup": "^3.18.0"
  }
}

Hello Worldの表示(プラグインのインストール)

webpack, parcelの時と同様にプロジェクトフォルダの直下にsrcフォルダを作成します。srcフォルダにindex.htmlとindex.jsファイルを作成してください。

それぞれに下記のコードを記述します。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <script type="module" src="../dist/main.js" defer></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

JSXではなくReact.createElementを利用してHello Worldを表示させます。


import React from 'react';
import ReactDOM from 'react-dom/client';

const App = React.createElement('h1', null, 'Hello World');

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App);

rollupでもコマンドラインを利用してビルドを行うことができるので-oオプションで出力先のファイルを指定します。下記のコマンドでsrc/index.jsファイル(entry file)をビルドしてdist/main.jsファイルが作成されます。

実行すると”Unresolved dependencies”というメッセージが表示されdistフォルダにはmain.jsファイルが作成されますが、中身はindex.jsファイルと同じです。


 % npx rollup src/index.js -o dist/main.js

src/index.js → dist/main.js...
(!) Unresolved dependencies
https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency
react (imported by "src/index.js")
react-dom/client (imported by "src/index.js")
created dist/main.js in 37ms

node_modulesに保存されたパッケージをimportして利用するためにはプラグインを利用する必要があります。プラグインは@rollup/plugin-node-resolveです。


 % npm install --save-dev @rollup/plugin-node-resolve

ここまではコマンドラインを利用していましたがrollupの設定ファイルを利用してビルドを行うためプロジェクトフォルダ直下にrollup.config.jsファイルを作成します。設定はコマンドラインで実行していた時と同じ内容になっていますがsourcemapを作成するのと先ほどインストールしたプラグイン@rollup/plugin-node-resolveを設定しています。


import resolve from '@rollup/plugin-node-resolve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [resolve()],
};

設定ファイルを作成してのでオプションに-configをつけることでrollupが自動でrollup.config.jsファイルを読み込んでくれます。–configの後に設定ファイルを指定することも可能です。その場合はファイル名に任意の名前をつけることができます。


 % npx rollup --config
(node:29443) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)
[!] RollupError: Node tried to load your configuration file as CommonJS even though it is likely an ES module. To resolve this, change the extension of your configuration to ".mjs", set "type": "module" in your package.json file or pass the "--bundleConfigAsCjs" flag.
//略

実行すると警告ができます。rollup.config.jsファイルはES module(exportを利用)していますがCommonJSとして設定ファイルを読み込もとしているのでエラーになっています。拡張子名をmjsまたはpackage.jsonのtypeオプションにmoduleを設定することで対応することができます。ここではpackage.jsonファイルにtypeオプションを追加してmoduleを設定します。


{
  "name": "create-react-app-rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@rollup/plugin-node-resolve": "^15.0.1",
    "rollup": "^3.18.0"
  }
}

再度ビルドコマンドを実行します。今度は異なるエラーが表示されます。


 % npx rollup --config
rollup v3.18.0
bundles src/index.js → dist/main.js...
[!] RollupError: "default" is not exported by "node_modules/react/index.js", imported by "src/index.js".
https://rollupjs.org/troubleshooting/#error-name-is-not-exported-by-module

エラーを解消するためには@rollup/plugin-commonjsをインストールする必要があります。


 % npm install --save-dev @rollup/plugin-commonjs

インストールが完了したらrollup.config.jsファイルのプラグインに追加します。


import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
  ],
};

再度ビルドコマンドを実行します。ビルドに成功します。


 % npx rollup --config                            

src/index.js → dist/main.js...
created dist/main.js in 1.9s

ビルドに成功したのでブラウザで動作確認を行うため開発サーバの設定を行います。rollup-plugin-serveのインストールを行います。

インストールしたrollup-plugin-serveはプラグインとして設定します。serveの引数には開発サーバの公開フォルダの設定を行っています。distに設定しているのでsrcフォルダのindex.htmlファイルをdistフォルダに移動します。


import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    serve('dist'),
  ],
};

移動したindex.htmlファイルではscriptタグのsrcのパスを変更します。ビルド後に作成されるdistフォルダのmain.jsを指定しています。


 <script type="module" src="./main.js" defer></script>

ビルドコマンドを実行するとポート番号10001で起動していることが確認できます。


 % npx rollup --config

src/index.js → dist/main.js...
http://localhost:10001 -> /Users/mac/Desktop/create-react-app-rollup/dist
created dist/main.js in 2s

ブラウザでlocalhost:10001にアクセスしますが画面上には”Hello World”は表示されません。デベロッパーツールのコンソールを確認すると下記のエラーが確認できます。


Uncaught ReferenceError: process is not defined
    at index.js:3:1
    at index.js:7:1

エラーを見るとif (process.env.NODE_ENV === ‘production’) の箇所でエラーが発生しています。processを定義するために@rollup/plugin-replaceをインストールする必要があります。


% npm install --save-dev @rollup/plugin-replace

インストールが完了したらrollup.config.jsでプラグインの設定を行います。下記の設定を行うことでバンドル中にprocess.env.NODE_ENVの値をdevelopementの文字列に置き換えてくれます。


import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve';
import replace from '@rollup/plugin-replace';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    serve('dist'),
    replace({
      'process.env.NODE_ENV': JSON.stringify('development'),
    }),
  ],
};

ビルドを実行すると@rollup/plugin-replaceに関するメッセージが表示されていますがビルドは成功しています。


 % npx rollup --config

src/index.js → dist/main.js...
http://localhost:10001 -> /Users/mac/Desktop/create-react-app-rollup/dist
(!) Plugin replace: @rollup/plugin-replace: 'preventAssignment' currently defaults to false. It is recommended to set this option to `true`, as the next major version will default this option to `true`.

ブラウザを見るとようやく”Hello World”の文字列を確認することができます。

Hello Worldの表示
Hello Worldの表示

ビルド時に表示されているメッセージを解消するためにreplaceのプラグインのオプションを追加します。


plugins: [
  resolve(),
  commonjs(),
  serve('dist'),
  replace({
    'process.env.NODE_ENV': JSON.stringify('development'),
    preventAssignment: true,
  }),
],

これで表示されているメッセージも解消されます。

更新する度にビルドコマンドを実行するのは効率が悪いのでオプションの-wを利用します。-wをつけることで更新を自動で検知してくれ自動でビルドを行ってくれます。


% npx rollup --config -w
rollup v3.18.0
bundles src/index.js → dist/main.js...
http://localhost:10001 -> /Users/mac/Desktop/create-react-app-rollup/dist
created dist/main.js in 2.3s

[2023-03-02 22:26:32] waiting for changes...

ファイルの更新を検知してビルドを自動で行ってくれますが開発サーバには反映されないので更新内容を確認するためにはブラウザのページのリロードが必要になります。自動でブラウザのリロードを行ってくれるrollup-plugin-livereloadをインストールします。


% npm install --save-dev rollup-plugin-livereload

インストール完了後、rollup.config.jsファイルでプラグインの追加を行います。


import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve';
import replace from '@rollup/plugin-replace';
import livereload from 'rollup-plugin-livereload';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    commonjs(),
    serve('dist'),
    replace({
      'process.env.NODE_ENV': JSON.stringify('development'),
      preventAssignment: true,
    }),
    livereload(),
  ],
};

rollup-plugin-livereloadを設定後index.jsファイルの”Hello World”の文字列を変更するなどして変更後にブラウザのリロードが自動でおこなわれるのか確認を行なってください。

process.env.NODE_ENVの文字列の置き換えをroll.config.jsファイルで行っていましたがpackage.jsonに追加するscriptsを利用して置き換える文字列を変更できるように設定します。–environmemtオプションで設定を行っています。


{
  "name": "create-react-app-rollup",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "rollup --config -w --environment NODE_ENV:development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^24.0.1",
    "@rollup/plugin-node-resolve": "^15.0.1",
    "@rollup/plugin-replace": "^5.0.2",
    "rollup": "^3.18.0",
    "rollup-plugin-livereload": "^2.0.5",
    "rollup-plugin-serve": "^2.0.2"
  }
}

rollup.config.jsファイルのプラグインのreplaceの引数の設定を文字列の”development”からprocess.env.NODE_ENVに更新します。


plugins: [
  resolve(),
  commonjs(),
  serve('dist'),
  replace({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
    preventAssignment: true,
  }),
  livereload(),
],

package.jsonファイルにscriptsのstartを追加したのでnpm startでビルドの実行と開発サーバの起動がおこなわれるようになります。

JSXを利用した場合

React.createElementではなくJSXで記述するためApp.jsファイルを作成して下記のコードを記述します。

App.jsファイルを新たに作成してJSXを利用した以下のコードを記述します。


import { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World</h1>;
};

export default App;

作成したApp.jsファイルはindex.jsファイルからimportします。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

npm startコマンドを実行するとビルドが再実行されますがJSXの<App />タグが理解できないためにエラーが出ています。


rollup v3.18.0
bundles src/index.js → dist/main.js...
[!] (plugin commonjs--resolver) SyntaxError: Unexpected token (6:12) in /Users/mac/Desktop/create-react-app-rollup/src/index.js
src/index.js (6:12)
4: const root = ReactDOM.createRoot(document.getElementById('root'));
5: 
6: root.render(<App />);
//略

JSXをJavaScriptに変換するためにwebpackと同様にbabelを利用します。rollupのbabelのプラグインとbabelのpreset-reactをインストールします。


 % npm install --save-dev @rollup/plugin-babel @babel/preset-react 

インストールしたbabelのプラグインの設定をrollup.config.jsファイルで行います。node_modulesのファイルをbabelの処理の対象外とするためexcludeを設定しています。presetsにはインストールした@babel/preset-reactを指定しています。


import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import serve from 'rollup-plugin-serve';
import replace from '@rollup/plugin-replace';
import livereload from 'rollup-plugin-livereload';
import babel from '@rollup/plugin-babel';

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/main.js',
    sourcemap: true,
  },
  plugins: [
    resolve(),
    babel({
      exclude: /node_modules/,
      presets: [['@babel/preset-react']],
    }),
    commonjs(),
    serve('dist'),
    replace({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
      preventAssignment: true,
    }),
    livereload(),
  ],
};

rollup.config.jsファイルを更新するとビルドが行われターミナルにはbableのオプションに関するメッセージが表示されます。ビルド自体は成功しています。


babelHelpers: 'bundled' option was used by default. It is recommended to configure this option explicitly, read more here: https://github.com/rollup/plugins/tree/master/packages/babel#babelhelpers
http://localhost:10001 -> /Users/mac/Desktop/create-react-app-rollup/dist
LiveReload enabled
created dist/main.js in 2.3s

ブラウザには何も表示されていないのでコンソールを確認すると以下のエラーが発生しています。Reactが見つかっていません。エラーがroot.render(<App />)の箇所で発生しており、JSXからJavaScriptの変換でReact.createElementを利用しようとしてReactがimportされていないのでエラーが発生しています。


index.js:6 Uncaught ReferenceError: React is not defined
    at index.js:6:13

React.createElementを実行できるようにindex.jsファイルとApp.jsファイルでReactをimportすればエラーは解消しますがbableのプラグインのオプションで対応します。runtimeにautomaticを設定します。runtimeはデフォルトではclassicが設定されておりcreateElementを利用するためにReactをimportする必要があります。React 17以降はコンパイル時にcreateElementとは異なる関数を利用するようになったためruntimeの値にautomaticを設定して新しい関数に対応させる必要があります。ビルド時のコンソールに表示されていたbabelHelpersの値についても”bundled”を設定しています。


plugins: [
  resolve(),
  babel({
    exclude: /node_modules/,
    presets: [['@babel/preset-react', { runtime: 'automatic' }]],
    babelHelpers: 'bundled',
  }),
  commonjs(),
  serve('dist'),
  replace({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
    preventAssignment: true,
  }),
  livereload(),
],

問題は解消しブラウザ上には”Hello World”が表示され、ブラウザのデベロッパーツールのコンソールには”test”の文字列が表示されます。

Rollupを利用してReactの開発環境を構築することができました。

esbuild

JavaScriptのモジュールバンドラーであるesbuildを利用してReactの環境を構築してみましょう。esbuildは他のバンドラー/ビルドツールとは異なりプログラム言語がGoで記述されており高速にビルドを行うことができます。ViteのPre-Bundlingでも利用されています。Viteにも利用されていますが実はまだバージョン1.0.0に達しておらず現在も開発が進められているツールです。

プロジェクトの作成

create-react-app-esbuildという名前のフォルダを作成します。フォルダ名は任意です。

create-react-app-esbuildに移動してnpm init -yコマンドを実行してpackage.jsonファイルを作成してください。


% npm init -y
Wrote to /Users/mac/Desktop/create-react-app-esbuild/package.json:

{
  "name": "create-react-app-esbuild",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Reactのインストール

reactとreact-domのパッケージをインストールします。


 % npm install react react-dom

インストール後にpackage.jsonファイルを確認するとdependenciesにインストールしたreact, react-domのパッケージを確認することができます。


{
  "name": "create-react-app-esbuild",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

esbuildのインストール

作成したプロジェクトでesbuildのインストールを行います。


 % npm install --save-dev esbuild

インストール直後のpackage.jsonファイルは下記の通りです。esbuildはdevDependenciesに追加されていることが確認できます。


{
  "name": "create-react-app-esbuild",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "esbuild": "^0.17.11"
  }
}

他のツールの時と同様にプロジェクトフォルダの直下にsrcフォルダを作成します。srcフォルダにindex.jsファイルを作成してください。

JSXではなくReact.createElementを利用してHello Worldを表示させます。


import React from 'react';
import ReactDOM from 'react-dom/client';

const App = React.createElement('h1', null, 'Hello World');

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App);

ビルド後に作成されるファイルをdistフォルダに作成するためdistフォルダを作成してindex.htmlファイルを作成します。scriptのsrcにはビルドに作成されるファイル名main.jsを指定しています。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <script type="module" src="./main.js" defer></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

ビルドコマンドを実行しますがビルドを行うと同時に開発サーバの起動を行います。ビルドを行うファイルをsrc/index.jsとしてビルド後に作成するファイルは–outfileで指定します。–servedirでは開発サーバの公開フォルダを指定しここではindex.htmlファイルを作成したdistを指定しています。


 % npx esbuild --bundle src/index.js --outfile=dist/main.js --servedir=dist

 > Local:   http://127.0.0.1:8000/
 > Network: http://192.168.2.191:8000/ 

コマンドを実行するとdistフォルダにmain.jsファイルが作成されることを確認することができます。ブラウザから開発サーバのhttp://127.0.0.1:8000にアクセスすると”Hello World”が表示されます。

esbuildでビルドを行う"Hello World"表示
esbuildでビルドを行う”Hello World”表示

コマンドに–watchオプションを追加することでファイルの更新を自動で検知しリビルドを行ってくれます。

JSXを利用した場合

React.createElementではなくJSXで記述するためApp.jsファイルを作成して下記のコードを記述します。

App.jsファイルを新たに作成してJSXを利用した以下のコードを記述します。


import { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World</h1>;
};

export default App;

作成したApp.jsファイルはindex.jsファイルからimportします。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

–watchオプションをつけてコマンドを実行している場合にはリビルドが行われ以下のメッセージが表示されます。ファイル名がjsなのでesbuildのloaderがjsxとして処理を行ってくれないためエラーとなっています。メッセージの通りオプションの–loader:.js=jsxをつけて実行します。


 [ERROR] The JSX syntax extension is not currently enabled

    src/index.js:6:12:
      6 │ root.render(<App />);
        ╵             ^

  The esbuild loader for this file is currently set to "js" but it must be set to "jsx" to be able
  to parse JSX syntax. You can use "--loader:.js=jsx" to do that.

–loader:.js=jsxをつけてesbuildコマンドを実行するとビルドは成功し、エラーが表示されなくなります。


% npx esbuild --bundle src/index.js --outfile=dist/main.js --servedir=dist --watch --loader:.js=jsx

しかし、ブラウザ上には何も表示されずブラウザのデベロッパーツールのコンソールにはエラーが表示されます。


main.js:23525 Uncaught ReferenceError: React is not defined
    at main.js:23525:31
    at main.js:23526:3

このエラーについてはrollupの時にも表示されている内容と同じなので何が原因かここまで読み進めている人であればわかるかと思います。

App.jsファイルとindex.jsファイルの先頭にimport React from ‘react’を追加してください。自動でリビルドが行われブラウザ上には”Hello World”が表示され、コンソールには”test”の文字列が表示されます。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

import React, { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World</h1>;
};

export default App;

Reactをimportする必要がある理由はJSXからJSに変換する際にReact.createElementを利用するためです。

esbuildではデフォルトでJSXからJSの変換がサポートされているので追加でプラグインなどをインストールする必要はありません。React 17以降はコンパイル時にcreateElementとは異なる関数を利用するようになったためimport Reactを削除するためにはコマンドラインに–jsx==automaticの設定が必要となります。


% npx esbuild --bundle src/index.js --outfile=dist/main.js --servedir=dist --watch --loader:.js=jsx --jsx=automatic

import React from “react”をindex.js, App.jsファイルから削除してもブラウザ上には”Hello World”が表示されます。

コマンドラインでloaderの設定を行っていましたがファイルの拡張子をjsからjsxに変更するとloaderオプションを外すことができます。


% npx esbuild --bundle src/index.jsx --outfile=dist/main.js --servedir=dist --watch  --jsx=automatic

設定ファイルの作成

オプションが増えてコマンドが長くなってきたのでコマンドのオプションで設定していた値を設定ファイルを利用して設定することができます。

プロジェクトフォルダにbuilder.jsファイルを作成します。


import * as esbuild from 'esbuild';

let ctx = await esbuild.context({
  entryPoints: ['src/index.jsx'],
  jsx: 'automatic',
  bundle: true,
  outfile: 'dist/main.js',
});

await ctx.watch();
console.log('watching...');

const server = await ctx.serve({
  servedir: 'dist',
  host: '127.0.0.1',
});
console.log(`server is running ${server.host}:${server.port}`);

実行はnode builder.jsコマンドで行います。実行すると以下のエラーメッセージが表示されます。builder.jsはimportを利用しているのでpackage.jsonファイルにtypeオプションを追加してmoduleを設定するかファイルの拡張子をbuilder.jsからbuilder.mjsに変更する必要があります。


(node:10901) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
(Use `node --trace-warnings ...` to show where the warning was created)

設定変更後再度実行すると開発サーバが起動してブラウザに”Hello World”が表示されます。


 % node build.js
watching...
server is running 127.0.0.1:8000

esbuildを利用してReactの開発環境を構築することができました。

Snowpack

JavaScriptのビルドツールであるSnowpackを利用してReactの環境を構築してみましょう。Snowpackは現在開発を行なっておらず新たにプロジェクトを作成することを推奨していません。代わりにViteの利用を推奨しています。

Snowpackではこれまで説明してきたwebpack, parcel, rollup, esbuildとは異なりindex.jsファイルをバンドルして複数のファイルをまとめていましたが開発時はsnowpackでは複数のファイルをバンドルという処理を行いません。更新を行なったファイルのみビルドを行うため高速に動作します。

実際にReact環境を構築することでバンドルしないということはどういうことなのかも含めて理解することができます。

プロジェクトの作成

create-react-app-snowpackという名前のフォルダを作成します。フォルダ名は任意です。

create-react-app-snowpackに移動してnpm init -yコマンドを実行してpackage.jsonファイルを作成してください。


 % npm init -y
Wrote to /Users/mac/Desktop/create-react-app-snowpack/package.json:

{
  "name": "create-react-app-snowpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Reactのインストール

reactとreact-domのパッケージをインストールします。


 % npm install react react-dom

インストール後にpackage.jsonファイルを確認するとdependenciesにインストールしたreact, react-domのパッケージを確認することができます。


{
  "name": "create-react-app-snowpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
}

Snowpackのインストール

作成したプロジェクトでSnowpackのインストールを行います。


 % npm install --save-dev snowpack

インストール直後のpackage.jsonファイルは下記の通りです。snowpackはdevDependenciesに追加されていることが確認できます。


{
  "name": "create-react-app-snowpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "snowpack": "^3.8.8"
  }
}

他のツールの時と同様にプロジェクトフォルダの直下にsrcフォルダを作成します。srcフォルダにindex.jsファイルを作成してください。


import React from 'react';
import ReactDOM from 'react-dom/client';

const App = React.createElement('h1', null, 'Hello World');

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(App);

index.htmlファイルをプロジェクトフォルダの直下に作成します。他のツールではscriptタグにビルド後のファイルを指定していましたがSnowpackではsrc/index.jsファイルを指定します。


<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <script type="module" src="./src/index.js" defer></script>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

npx snowpack devコマンドを実行すると開発サーバが起動します。


 % npx snowpack dev
[12:21:20] [snowpack] Hint: run "snowpack init" to create a project config file. Using defaults...
[12:21:20] [snowpack] Ready!
[12:21:20] [snowpack] Server started in 61ms.
[12:21:20] [snowpack] Local: http://localhost:8080
[12:21:20] [snowpack] Network: http://192.168.2.191:8080

ブラウザには”Hello World”が表示されます。npx snowpack devコマンドではデフォルトでプロジェクトフォルダの直下を公開フォルダとするためindex.htmlファイルが読み込まれて表示されます。

Snowpackを利用してHello World
Snowpackを利用してHello World

プロジェクトフォルダ下に新たにフォルダが作成されたり、ファイルが作成されたりはしません。

JSXを利用した場合

React.createElementではなくJSXで記述するためApp.jsファイルを作成して下記のコードを記述します。


import React, { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World!</h1>;
};

export default App;

作成したApp.jsファイルはindex.jsファイルからimportします。


import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

npx snowpack devコマンドを実行するとエラーもなく開発サーバが起動しますがブラウザのデベロッパーツールのコンソールには”Uncaught SyntaxError: Unexpected token ‘<‘ (at index.js:6:13)”が表示されJSXタグが認識できていないことがわかります。ブラウザはJSXを認識することができないためJSXからJSへの変換が必要になります。SnowpackではデフォルトでJSXかJSに変換する機能を持っているので機能を有効化させるためにはにファイルの拡張子をjsからjsxに変更する必要があります。

拡張子を変更するとコンソールには”Uncaught ReferenceError: React is not defined”が表示されエラーメッセージが変わります。

このエラーについてはrollup, esbuildの時にも表示されている内容と同じなので何が原因かここまで読み進めている人であればわかるかと思います。

原因はJSXからJSに変換する際にReact.createElementを利用するためなのでApp.jsファイルとindex.jsファイルの先頭にimport React from ‘react’を追加してください。


import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(<App />);

import React, { useEffect } from 'react';
const App = () => {
  useEffect(() => {
    console.log('test');
  }, []);

  return <h1>Hello World!</h1>;
};

export default App;

設定後、ブラウザ上には”Hello World”が表示されデベロッパーツールのコンソールには”test”が表示されます。

確認した通りindex.jsxとApp.jsxの2つのファイルで構成されていますがSnowpackはバンドルを行いません。バンドルを行わずにどのようにして処理が行われているのか確認するためにブラウザのデベロッパーツールのネットワークタブを確認します。index.jsとApp.jsフィルが別々にダウンロードされていることがわかります。さらにindex.jsファイルとApp.jsファイルだけではなくreactやreact-domのファイルも確認することができます。

複数のJavaScriptファイルをダウンロード
複数のJavaScriptファイルをダウンロード

App.jsファイルの中身を確認するとApp.jsxのファイルに記載したコードのままではなくJSXのコードがReact.createElementに変換されていることがわかります。

jSXからJSへの変換が行われている
jSXからJSへの変換が行われている

バンドルは行わず、ファイル毎にビルドを行ってブラウザ上に”Hello World”を表示させていることがわかります。

設定ファイルの作成

コマンドを利用してSnowpackの設定ファイルを作成します。snowpackコマンドにinitオプションをつけて実行するとsnowpack.config.jsファイルが作成されます。


 % npx snowpack init

// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration

/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
  mount: {
    /* ... */
  },
  plugins: [
    /* ... */
  ],
  packageOptions: {
    /* ... */
  },
  devOptions: {
    /* ... */
  },
  buildOptions: {
    /* ... */
  },
};

引き続き作成中です。