はじめに

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 / exportwindow を使ったデータの受け渡しが可能ですが、“動的な状態の変化” を扱うには向いていないという特徴があります。

その理由は大きく 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 / exportwindow を使った共有 には「通知の仕組み」がない

import / exportwindow を使った共有はあくまで「値や関数を共有するための仕組み」であり、

  • 値が変わった
  • 処理が完了した
  • 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 が実務的で扱いやすい。