はじめに
ES Modules を使うと、JavaScript のコードはファイルごとに独立した “モジュールスコープ” を持つようになります。 これは安全で扱いやすい仕組みですが、同時に 「別ファイルの変数が見えない」 という新しい問題も生まれます。
この章では、モジュール間でデータを共有するための実践的な方法を、実際のコード例を交えながら解説します。
モジュールスコープとは何か
スコープとは、変数や関数が有効である範囲を指します。
ES Modules では、ファイルごとに独立したスコープが作られます。そのため、モジュール内で宣言した変数は window に自動では載らず、通常、外部からは参照できません。
これをモジュールスコープと呼びます。
module.js
const count = 1;
main.js
import "./module.js";
console.log(window.count); // undefined
👉 これは ES Modules の設計思想である「グローバル汚染を防ぐ」という目的によるものです。
この性質があるため、モジュール間でデータを共有したい場合は、明示的な仕組みが必要になります。
モジュール → 通常のスクリプトへのデータ共有
モジュールから通常のスクリプトにデータを共有したい場合、明示的に Window に公開する必要があります。
window に明示的に載せる
モジュール内の変数は自動では外に出ませんが、意図的に window に公開することは可能です。
state.js (module)
const state = {
user: 'Kon',
theme: 'light'
};
window.appState = state;
main.js
import "./state.js";
console.log(window.appState.user); // Kon
どんな時に使うか?
- 複数のモジュールから共通状態を参照したい。
- 小規模アプリで「アプリ全体の状態」を
windowに置きたい。 - Eleventy の静的サイトで「ビルド時に埋め込んだ設定」を参照したい。
- Web Components や
iframeと連携したい。
👉 小規模〜中規模のアプリでは、window に状態を置くのは実務でもよく使われるパターンです。
通常のスクリプト → モジュールへのデータ共有
逆に、通常のスクリプトからモジュールにデータを共有したい場合、window に置かれた値を参照できます。
window に置いた値をモジュール側で読む
通常のスクリプト(type を付けない <script>)では、var で宣言した変数は自動的に window に載ります。そのため、モジュール側から window.user のように参照できます。
module.js
export function display() {
console.log(window.user); // "Kon"
}
main.js
var user = "Kon";
import { display } from "./module.js";
display();
実務でよくある使い方
- 初期設定(config)を
windowに置く - ログイン情報やユーザー設定を
windowに置く - ページ全体で共有したい状態を
windowに集約する
👉 特に Eleventy のような静的サイトでは、ビルド時に生成したデータを window に埋め込むというパターンが自然に使えます。
CustomEvent を使った通知
CustomEvent は、各モジュール間で便利にやりとりが行える仕組みです。通知という形で、値の受け渡しも可能(「値そのものを渡すのではなく、detail に載せて渡す)です。
なぜ CustomEvent が必要なのか?
ES Modules においては、 import / export や window を使ったデータの受け渡しが可能ですが、“動的な状態の変化” を扱うには向いていないという特徴があります。
その理由は大きく 2 つあります。
1. import / export は「静的な依存関係」
import は、どのモジュールがどのモジュールを使うかを “読み込み時(ロード時)” に決める仕組みです。
つまり、
- どの値を参照するか
- どの関数を使うか
といった依存関係は 実行前に固定される ため、実行中に値が変わっても、自動では反映されません。
state.js
export let count = 0;
export function increment() {
count++;
}
main.js
import { count, increment } from './state.js';
console.log(count); // 0
increment();
console.log(count); // 0 のまま(更新されない)
increment() 内では確かに count が増えていますが、import した側の count は 再評価されないため、値が変わったことに気づけません。
2. import / export や window を使った共有 には「通知の仕組み」がない
import / export や window を使った共有はあくまで「値や関数を共有するための仕組み」であり、
- 値が変わった
- 処理が完了した
- UI を更新してほしい
といった “変化を知らせる” ための仕組みはありません。
そのため、 A モジュールで状態が変わったら B モジュールに「変わったよ」と知らせたいという場面では、これらの仕組みでは不十分です。
では、どうすれば良いか?
そこで、CustomEvent のような通知の仕組みが必要になります。
CustomEvent は、こうした「動的な変化を別のモジュールに伝えたい」という場面に最適です。
CustomEvent の特徴
- モジュール間の疎結合な通信ができる。
- データを
detailに載せて受け渡しができる。 - Web Components と相性が良い。
- IndexedDB の更新通知にも使われる。(実務で多い)
👉 CustomEvent は、同じタブ内での“グローバルイベントバス” として使えるため、モジュール間通信の中心的な手段になります。
例:状態が変わったら通知する
state.js
let count = 0;
export function increment() {
count++;
window.dispatchEvent(
new CustomEvent('count:changed', { detail: count })
);
}
ui.js
window.addEventListener('count:changed', (e) => {
console.log('count changed:', e.detail);
});
main.js
import {increment} from "./state.js";
import "./ui.js";
increment(); // 1
increment(); // 2
これで、状態の変化に応じた UI 更新といった “動的な処理” を安全に実現できます。
実務でよく使われるパターンまとめ
パターン1: window にアプリ状態を置く
小規模アプリで最も多い構成です。
window.app = {
db,
config,
user,
};
パターン2:CustomEvent で通知する
状態の変化を他のモジュールに知らせるための仕組み。
- IndexedDB の更新通知
- Web Components のイベント
- UI の状態更新
パターン3: import / export は “変わらないもの” に限定
- 定数
- ユーティリティ関数
- クラス
- 設定値(config)
パターン4:アプリの初期化は 1 箇所にまとめる
main.js(エントリーポイント)で
windowに状態を置く- イベントリスナーを登録
- モジュールを初期化
という構成が最も安定します。
まとめ
ES Modules はファイルごとに独立したスコープを持つため、データ共有には明示的な仕組みが必要です。
windowを使うと “共有” ができる。CustomEventを使うと “通知” ができる。import/exportは静的な依存関係に向いている。- 小規模〜中規模アプリでは、
window+CustomEventが実務的で扱いやすい。