Node.jsやReactで開発していて、突然「Cannot access 'X' before initialization」や「undefined is not a function」といったエラーに遭遇したことはありませんか?コードは正しく書けているはずなのに、なぜかモジュールの読み込みでエラーが出る…その原因は循環参照(Circular Dependencies)にあるかもしれません。
この記事では、循環参照の仕組みを理解し、ESLintやツールを使って効率的に検出・解決する方法を実践的に解説します。
GitHub

GitHub - import-js/eslint-plugin-import: ESLint plugin with rules that help validate proper imports.
ESLint plugin with rules that help validate proper imports. - import-js/eslint-plugin-import
目次
循環参照エラーの症状と原因を理解しよう
よくあるエラーメッセージと発生パターン
循環参照によって引き起こされる典型的なエラーは以下のようなものです:
// よく見るエラーメッセージ
Cannot access 'ClassA' before initialization
TypeError: undefined is not a function
TypeError: Cannot read properties of undefined
ReferenceError: X is not defined
これらのエラーは、以下のような状況で発生します:
- Node.js開発中:モジュール間でお互いを
requireし合っている - Webpackビルド時:バンドル時に「Circular dependency detected」警告が大量発生
- React/Vue.js:コンポーネント間の循環参照でアプリが動かない
- TypeScript:型定義間の循環参照でコンパイルエラー
CommonJSとES6 Modulesでの違い
// CommonJS(Node.js)の場合
// a.js
const b = require('./b.js');
module.exports = { name: 'A' };
// b.js
const a = require('./a.js'); // この時点でa.jsは完全に読み込まれていない
console.log(a); // {} 空オブジェクトが返される
// ES6 Modules の場合
// a.js
import { b } from './b.js';
export const a = 'A';
// b.js
import { a } from './a.js'; // ReferenceError: Cannot access 'a' before initialization
export const b = 'B';
なぜ循環参照が問題になるのか
循環参照が問題となる根本的な理由は、モジュールの読み込みタイミングにあります:
| 実行順序 | 何が起こるか | 結果 |
|---|---|---|
| 1. A.jsの読み込み開始 | B.jsをimport/require | A.jsの処理が一時停止 |
| 2. B.jsの読み込み開始 | A.jsをimport/require | A.jsはまだ初期化途中 |
| 3. 循環参照の検知 | 不完全なA.jsが返される | undefined や空オブジェクト |
緊急解決!3つの即効対処法
循環参照エラーが今まさに発生している場合、以下の方法で即座に解決できます。
方法1: Lazy require による回避
// ❌ 問題のあるコード
const UserService = require('./UserService');
class CustomerService {
getUser() {
return UserService.find(); // エラー:UserServiceが不完全
}
}
// ✅ 修正後:使う直前でrequire
class CustomerService {
getUser() {
const UserService = require('./UserService'); // lazy require
return UserService.find(); // 正常に動作
}
}
方法2: module.exports の順序調整
// ❌ 問題のあるコード
const ClassB = require('./ClassB');
class ClassA {
// クラス定義
}
module.exports = ClassA; // requireの後にexports
// ✅ 修正後:exportsを先に定義
class ClassA {
// クラス定義
}
module.exports = ClassA; // requireの前にexports
const ClassB = require('./ClassB');
方法3: Dynamic import を使った解決
// ES6 Modulesでの解決法
export class ComponentA {
async loadComponentB() {
const { ComponentB } = await import('./ComponentB.js'); // dynamic import
return new ComponentB();
}
}
ESLint import/no-cycle で循環参照を予防
eslint-plugin-import の設定方法
# パッケージのインストール
npm install --save-dev eslint-plugin-import
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:import/recommended"
],
"plugins": ["import"],
"rules": {
"import/no-cycle": ["error", {
"maxDepth": 10,
"ignoreExternal": true
}]
}
}
TypeScriptプロジェクトでの設定
// TypeScript用の設定
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript"
],
"parser": "@typescript-eslint/parser",
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
"typescript": true,
"node": true
}
},
"rules": {
"import/no-cycle": ["error", {
"maxDepth": 10,
"ignoreExternal": true,
"allowUnsafeDynamicCyclicDependency": false
}]
}
}
設定オプションの使い分け
| オプション | 説明 | 推奨設定 |
|---|---|---|
maxDepth |
検出する循環参照の深さ | 10(パフォーマンス考慮) |
ignoreExternal |
node_modulesを除外 | true(通常は除外) |
allowUnsafeDynamicCyclicDependency |
dynamic importの循環を許可 | false(安全性重視) |
検出ツールで循環参照を可視化
Madge による依存関係の可視化
あわせて読みたい
# Madgeのインストール
npm install -g madge
# 循環参照を検出
madge --circular src/
# 視覚化された依存関係グラフを生成
madge --image dependency-graph.svg src/
# 特定のファイルから始まる循環参照を調査
madge --circular --json src/ > circular-deps.json
// プログラムから使用する場合
const madge = require('madge');
madge('src/')
.then((res) => {
const circular = res.circular();
if (circular.length > 0) {
console.log('循環参照が発見されました:');
circular.forEach(cycle => {
console.log(cycle.join(' → '));
});
}
});
Webpack circular-dependency-plugin の活用
あわせて読みたい
// webpack.config.js
const CircularDependencyPlugin = require('circular-dependency-plugin');
module.exports = {
plugins: [
new CircularDependencyPlugin({
// node_modulesを除外
exclude: /node_modules/,
// 検出時にビルドを失敗させる
failOnError: true,
// より詳細な情報を表示
allowAsyncCycles: false,
cwd: process.cwd(),
// カスタム処理を追加
onDetected({ module: webpackModuleRecord, paths, compilation }) {
compilation.warnings.push(new Error(paths.join(' -> ')));
}
})
]
};
CI/CDパイプラインへの組み込み
# GitHub Actions での例
name: Circular Dependency Check
on: [push, pull_request]
jobs:
check-circular-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npx madge --circular src/
- run: npm run lint # ESLint with import/no-cycle
React Hooks での循環参照問題
useEffect 無限ループの回避
// ❌ 無限ループを引き起こすコード
function BadComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
useEffect(() => {
setItems([...items, count]); // itemsを依存配列に含めると無限ループ
}, [count, items]); // 危険:itemsが変更されるたびに実行
return <div>{items.length}</div>;
}
// ✅ 修正後:関数形式のsetStateを使用
function GoodComponent() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
useEffect(() => {
setItems(prevItems => [...prevItems, count]); // 前の状態を参照
}, [count]); // countの変更時のみ実行
return <div>{items.length}</div>;
}
Custom Hooks での循環参照対策
// カスタムフック:useNoRenderRef
function useNoRenderRef(currentValue) {
const ref = useRef(currentValue);
ref.current = currentValue;
return ref;
}
// 使用例
function ComponentWithNoLoop() {
const [item, setItem] = useState(0);
const [queue, setQueue] = useState([]);
const queueRef = useNoRenderRef(queue);
useEffect(() => {
// refを使用して循環参照を回避
setQueue(queueRef.current.concat(item));
}, [item, queueRef]);
return <div>Queue length: {queue.length}</div>;
}
useRef を使った解決パターン
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const searchTimeoutRef = useRef();
useEffect(() => {
// 前のタイムアウトをクリア
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
// 新しいタイムアウトを設定
searchTimeoutRef.current = setTimeout(() => {
// 検索実行(循環参照を回避)
performSearch(searchTerm).then(setResults);
}, 300);
// クリーンアップ
return () => {
if (searchTimeoutRef.current) {
clearTimeout(searchTimeoutRef.current);
}
};
}, [searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div>{results.length} results</div>
</div>
);
}
根本解決:設計レベルでの循環参照回避
Dependency Injection パターン
// ❌ 循環参照のあるコード
// UserService.js
const OrderService = require('./OrderService');
class UserService {
getUserOrders(userId) {
return OrderService.getByUserId(userId);
}
}
// OrderService.js
const UserService = require('./UserService');
class OrderService {
getOrderWithUser(orderId) {
const order = this.getById(orderId);
return {
...order,
user: UserService.getById(order.userId)
};
}
}
// ✅ DIパターンによる解決
// services/index.js(サービスコンテナ)
class ServiceContainer {
constructor() {
this.services = {};
}
register(name, service) {
this.services[name] = service;
}
get(name) {
return this.services[name];
}
}
const container = new ServiceContainer();
// UserService.js
class UserService {
constructor(container) {
this.container = container;
}
getUserOrders(userId) {
const orderService = this.container.get('OrderService');
return orderService.getByUserId(userId);
}
}
// 初期化時にコンテナに登録
container.register('UserService', new UserService(container));
container.register('OrderService', new OrderService(container));
レイヤードアーキテクチャの採用
プロジェクト構造例:
src/
├── presentation/ # UI層(React Components)
│ ├── components/
│ └── pages/
├── application/ # アプリケーション層(Business Logic)
│ ├── services/
│ └── usecases/
├── domain/ # ドメイン層(Entities, Value Objects)
│ ├── entities/
│ └── repositories/
└── infrastructure/ # インフラ層(DB, API)
├── api/
└── database/
このような層分けにより、以下のルールを適用:
- 上位層は下位層を参照できる(presentation → application → domain → infrastructure)
- 下位層は上位層を参照しない(循環参照の防止)
- 同一層内での循環参照は避ける
ファイル分割のベストプラクティス
// ✅ 共通の依存関係を別ファイルに分離
// shared/types.js
export const USER_ROLES = {
ADMIN: 'admin',
USER: 'user'
};
export const validateUser = (user) => {
// 共通のバリデーションロジック
};
// UserService.js
import { USER_ROLES, validateUser } from '../shared/types.js';
// OrderServiceは直接importしない
// OrderService.js
import { USER_ROLES, validateUser } from '../shared/types.js';
// UserServiceは直接importしない
// UserOrderService.js(新しいファイル)
import { UserService } from './UserService.js';
import { OrderService } from './OrderService.js';
export class UserOrderService {
constructor(userService, orderService) {
this.userService = userService;
this.orderService = orderService;
}
getUserWithOrders(userId) {
const user = this.userService.getById(userId);
const orders = this.orderService.getByUserId(userId);
return { user, orders };
}
}
イベント駆動アーキテクチャの活用
// EventEmitterを使った疎結合な設計
const EventEmitter = require('events');
const eventBus = new EventEmitter();
// UserService.js
class UserService {
createUser(userData) {
const user = this.create(userData);
// 他のサービスに直接依存せず、イベントを発行
eventBus.emit('user:created', user);
return user;
}
}
// NotificationService.js
class NotificationService {
constructor() {
// イベントをリッスンして処理
eventBus.on('user:created', this.sendWelcomeEmail.bind(this));
}
sendWelcomeEmail(user) {
// ウェルカムメール送信処理
}
}
まとめ:循環参照のない健全なコードベースを維持する
循環参照は複雑なアプリケーションでは避けられない問題ですが、適切なツールと設計パターンを使用することで効果的に管理できます。
重要なポイント
- 早期発見:ESLint import/no-cycleとMadgeで開発時に検出
- 即座解決:Lazy require、Dynamic import、モジュール順序調整
- 根本解決:DI、レイヤードアーキテクチャ、イベント駆動設計
- 継続監視:CI/CDパイプラインでの自動チェック
今後のアクション
- プロジェクトにESLint import/no-cycleを導入
- Madgeで既存の循環参照を調査
- Webpack circular-dependency-pluginでビルド時チェック
- 設計レベルでの改善を検討
循環参照は「コードスメル」の一種でもあります。発見したら単純に回避するだけでなく、なぜその循環が生まれたのかを考え、より良い設計を目指していきましょう。
