Fix: Circular Dependency in Plugins in Nuxt

Error message:
Circular dependency detected in plugins: {visited} -> {name}
plugins 2025-01-25

What Causes This Error?

This error occurs when two or more plugins depend on each other in a circular way:

  • Plugin A depends on Plugin B
  • Plugin B depends on Plugin A

Nuxt can’t determine which to load first, causing an infinite loop.

The Problem

// plugins/a.ts
export default defineNuxtPlugin({
  name: 'plugin-a',
  dependsOn: ['plugin-b'],  // A depends on B
  setup() {
    console.log('Plugin A')
  }
})

// plugins/b.ts
export default defineNuxtPlugin({
  name: 'plugin-b',
  dependsOn: ['plugin-a'],  // B depends on A - CIRCULAR!
  setup() {
    console.log('Plugin B')
  }
})

How to Fix It

Option 1: Remove Unnecessary Dependency

Often, one of the dependencies isn’t actually needed:

// plugins/a.ts
export default defineNuxtPlugin({
  name: 'plugin-a',
  // Removed dependency on plugin-b
  setup() {
    console.log('Plugin A')
  }
})

// plugins/b.ts
export default defineNuxtPlugin({
  name: 'plugin-b',
  dependsOn: ['plugin-a'],  // Only B depends on A
  setup() {
    console.log('Plugin B')
  }
})

Option 2: Extract Shared Logic

Create a third plugin with the shared functionality:

// plugins/shared.ts
export default defineNuxtPlugin({
  name: 'plugin-shared',
  setup() {
    // Shared logic that both A and B need
    return {
      provide: {
        sharedUtil: () => 'shared'
      }
    }
  }
})

// plugins/a.ts
export default defineNuxtPlugin({
  name: 'plugin-a',
  dependsOn: ['plugin-shared'],
  setup() {
    const { $sharedUtil } = useNuxtApp()
    // Use shared utility
  }
})

// plugins/b.ts
export default defineNuxtPlugin({
  name: 'plugin-b',
  dependsOn: ['plugin-shared'],
  setup() {
    const { $sharedUtil } = useNuxtApp()
    // Use shared utility
  }
})

Option 3: Use Lazy Initialization

Instead of dependencies, use lazy access:

// plugins/a.ts
export default defineNuxtPlugin({
  name: 'plugin-a',
  setup(nuxtApp) {
    return {
      provide: {
        a: {
          getValue() {
            // Lazily access plugin B when needed
            return nuxtApp.$b?.someValue
          }
        }
      }
    }
  }
})

// plugins/b.ts
export default defineNuxtPlugin({
  name: 'plugin-b',
  setup(nuxtApp) {
    return {
      provide: {
        b: {
          someValue: 42,
          getValue() {
            // Lazily access plugin A when needed
            return nuxtApp.$a?.getValue()
          }
        }
      }
    }
  }
})

Option 4: Merge Plugins

If plugins are tightly coupled, combine them:

// plugins/combined.ts
export default defineNuxtPlugin({
  name: 'plugin-combined',
  setup() {
    // Logic from plugin A
    const aFeature = () => 'A'

    // Logic from plugin B
    const bFeature = () => 'B'

    // They can now reference each other freely
    const combined = () => aFeature() + bFeature()

    return {
      provide: {
        a: { feature: aFeature },
        b: { feature: bFeature },
        combined
      }
    }
  }
})

Option 5: Use Events/Hooks

Decouple using events:

// plugins/a.ts
export default defineNuxtPlugin({
  name: 'plugin-a',
  setup(nuxtApp) {
    // Listen for event from B
    nuxtApp.hook('b:ready', (bData) => {
      console.log('B is ready:', bData)
    })

    // Emit when A is ready
    nuxtApp.callHook('a:ready', { value: 'A' })
  }
})

// plugins/b.ts
export default defineNuxtPlugin({
  name: 'plugin-b',
  setup(nuxtApp) {
    // Listen for event from A
    nuxtApp.hook('a:ready', (aData) => {
      console.log('A is ready:', aData)
    })

    // Emit when B is ready
    nuxtApp.callHook('b:ready', { value: 'B' })
  }
})

Debugging Circular Dependencies

Visualize the Dependency Graph

// In nuxt.config.ts
export default defineNuxtConfig({
  hooks: {
    'app:resolve'(app) {
      console.log('Plugins:', app.plugins.map(p => ({
        src: p.src,
        name: p.name
      })))
    }
  }
})

Check Plugin Load Order

// plugins/debug.ts
export default defineNuxtPlugin({
  name: 'plugin-debug',
  enforce: 'pre',  // Load first
  setup() {
    console.log('Debug plugin loaded')
  }
})

Plugin Ordering Without Dependencies

Use enforce to control order without explicit dependencies:

// plugins/first.ts
export default defineNuxtPlugin({
  name: 'first',
  enforce: 'pre',  // Runs before default plugins
  setup() {}
})

// plugins/middle.ts
export default defineNuxtPlugin({
  name: 'middle',
  // enforce: 'default' is implied
  setup() {}
})

// plugins/last.ts
export default defineNuxtPlugin({
  name: 'last',
  enforce: 'post',  // Runs after default plugins
  setup() {}
})

Best Practices

  1. Minimize dependencies - Only depend on what you truly need
  2. Use composition - Create small, focused plugins
  3. Prefer loose coupling - Use events/hooks instead of direct dependencies
  4. Document dependencies - Make plugin relationships clear
  5. Test plugin order - Verify plugins load correctly

Quick Checklist

  • Identify the circular dependency chain
  • Determine which dependency can be removed
  • Consider extracting shared logic to a base plugin
  • Use lazy initialization for cross-plugin access
  • Consider merging tightly coupled plugins
  • Use enforce for ordering without dependencies