Vueの開発業務で、循環依存エラーの解決の際に依存性注入を知ったのと、プログラミングでよく出てくる 「依存」 をこの機会に調べたので、記事にまとめました。
そもそも「依存」とは?
依存(Dependency) とは、あるモジュール(ファイル・関数・クラス)が他のモジュールの機能を使っている(依存している)状態のこと。
自動車
↓ 使う(依存)
エンジン
↓ 使う(依存)
ガソリン
| 依存 | あるモジュールが他のモジュールの機能を使っている状態 |
| 依存関係 | どのモジュールがどのモジュールに依存しているかの関係 |
| 依存する側 | 機能を使う側(import する側) |
| 依存される側 | 機能を提供する側(import される側) |
| 直接依存 | 直接 import して使う |
| 間接依存 | 他のモジュールを経由して間接的に使う |
| 循環依存 | お互いに依存し合っている(本記事のテーマ) |
| 密結合 | 依存が強い(変更の影響が大きい) |
| 疎結合 | 依存が弱い(変更の影響が小さい) |
依存関係は同一モジュール内でも発生します。
例:関数間の依存
// utils.tsに以下2つの関数が定義されている
const calculateTax = (price: number): number => {
return price * 0.1
}
const calculateTotal = (price: number, quantity: number): number => {
const subtotal = price * quantity
const tax = calculateTax(subtotal) // ← calculateTax に依存
return subtotal + tax
}
- calculateTotalは依存する側で、calculateTaxは依存される側
- calculateTotal は calculateTax に依存している
- calculateTax がないと calculateTotal は動かない
- calculateTax の仕様が変わると calculateTotal に影響する
循環依存とは
2つ以上のモジュール(ファイル)がお互いに依存し合っている状態
分かりやすく言い換えると、2つ以上のモジュール(ファイル)がお互いにインポート(import)し合うことで発生するエラー。

両方が相手を必要としているため、どちらを先に初期化すればいいか分からず、エラーになります。
ストアとフックに限らす、フック同士でも起きる
*コンポーネント同士でもおきますが、コンポーネントを相互にインポートしあう事はあまりないかと思います。
循環依存エラーが起きるケース
// Store
import { useDoubleCounter } from '@/composables/useDoubleCounter' // ❌ hooksをインポート
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
increment = () => {
count.value++
}
// ❌ hooksを使用
incrementDouble = () => {
const { doubleAndIncrement } = useDoubleCounter()
doubleAndIncrement()
}
return {
count,
increment,
incrementDouble
}
})
// hooks
import { useCounterStore } from '@/stores/counter' // ❌ Storeをインポート
export function useDoubleCounter() {
doubleAndIncrement = () => {
const store = useCounterStore() // ❌ Storeを使用
// カウントを2倍にしてからインクリメント
store.count = store.count * 2
store.increment() // ❌ Storeのメソッドを使用
}
return { doubleAndIncrement }
}
Store → Hooks → Store の相互依存で初期化順序が決まらないのが原因。
依存性注入(Dependency Injection, DI)とは
オブジェクトやモジュールが必要とする依存関係を、外部から注入(渡す)する設計パターン
注入は依存関係を外部から渡すこと全般を指します。
// 関数を引数で渡す
useValidation = (updateFn: (value: string) => void) => {
updateFn('test') // 注入された関数を使用
}
useValidation((value) => console.log(value)) // 関数を注入
// コンストラクタで渡す
class UserService {
constructor(private repository: UserRepository) {}
}
const service = new UserService(new UserRepository())
つまりhooksの内部で必要なメソッドをストアからインポートするのではなく、ストアから注入してもらう
依存性注入により循環依存エラーを解決
前述の循環依存エラーを依存性注入で解決するサンプル
// Store
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
increment = () => {
count.value++
}
// ✅ hooksを初期化する際に必要なメソッド(依存)を注入
incrementDouble = () => {
const { doubleAndIncrement } = useDoubleCounter({
count: count.value,
increment
})
doubleAndIncrement()
}
return {
count,
increment,
incrementDouble
}
})
// hooks
// ❌ import { useCounterStore } from '@/stores/counter'
// ✅ Storeをインポートしない
interface DoubleCounterDeps {
count: number
increment: () => void
}
export function useDoubleCounter(deps?: DoubleCounterDeps) {
doubleAndIncrement = () => {
if (!deps) return
// ✅ 注入された値とメソッドを使用するので、Storeをインポートしなくていい
const doubled = deps.count * 2
console.log(`${deps.count} → ${doubled}`)
deps.increment()
}
return { doubleAndIncrement }
}
ポイント:
- hooksはStoreをimportしない
- Storeだけがhooksをimport
- Storeがhooksに必要なメソッドを渡す
これにより、一方向の依存になり、初期化順序が明確
*技術的には「コールバック関数」でもあるが、目的は「循環依存の解決」なので「依存性注入」と呼ぶ。
DI以外の解決策
Storeのメソッドを別のhooksに分離が可能であれば、Store側で両方のhooksをimportするのも手です。
