Fixed: Two Alpine.js Instances Detected in Laravel + Livewire

The problem: After adding Livewire to an existing Laravel app, Alpine-powered dropdowns, modals, and toggles start behaving erratically — some fire twice, some don't fire at all, and the console warns about multiple Alpine instances.

Environment: Laravel 11.x/13.x, an app originally scaffolded with Breeze (Blade + Alpine.js), Livewire 3.x/4.x added afterward.

The Error

▲ browser console
Alpine Warning: Alpine has already been initialized on this page.

// or, depending on load order, an x-data element silently never
// becomes interactive at all, with no warning shown whatsoever

Why It Happens

Since Livewire 3, Livewire ships its own bundled copy of Alpine.js internally — you no longer need to install Alpine separately at all, and Livewire boots it for you automatically. If your project already had Alpine set up before Livewire was added (very common when Livewire gets bolted onto an existing Breeze or Jetstream Blade app), you now have two independent Alpine instances trying to run on the same page:

  1. The Alpine bundled inside resources/js/app.js from the original Breeze scaffold (import Alpine from 'alpinejs'; Alpine.start();).
  2. The Alpine that Livewire boots itself, automatically, the moment its own script loads.

Whichever one initializes first calls Alpine.start() and takes ownership of every x-data element on the page. The second one to load either throws the "already initialized" warning, or — worse — silently loses the race and its own x-init/event bindings never attach, which is why some interactions look like they're firing twice while others don't fire at all.

The Fix

Remove the separate Alpine import from your app's JS entry file — let Livewire own Alpine entirely:

// resources/js/app.js

import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();

// delete all three lines above — Livewire boots Alpine for you

If you were pulling Alpine in via a CDN <script> tag in your layout (common in pre-Livewire-3 setups), remove that tag entirely once Livewire is installed:

<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>

<!-- remove this line — @livewireScripts already provides Alpine -->

Also remove any standalone Alpine plugin imports (like @alpinejs/persist or @alpinejs/focus) from your JS entry file — Livewire's bundled Alpine already includes every official plugin except @alpinejs/ui, so a separately imported plugin registers a second instance of that plugin too, not just a second Alpine core:

import Alpine from 'alpinejs';
import persist from '@alpinejs/persist';
Alpine.plugin(persist);

// remove — persist, focus, and other official plugins are already
// bundled with Livewire's Alpine, except @alpinejs/ui

If your app genuinely needs @alpinejs/ui (used by Alpine Components' headless UI primitives), that's the one plugin Livewire doesn't bundle — install and register just that plugin, and nothing else, alongside Livewire.

Step-by-Step

  1. Open resources/js/app.js and check for import Alpine from 'alpinejs' plus any Alpine.start() call — delete both if Livewire is also installed.
  2. Check your root Blade layout for a CDN <script> tag loading Alpine directly, and remove it if present.
  3. Search for any @alpinejs/* plugin imports and remove them too, unless the plugin is specifically @alpinejs/ui.
  4. Confirm @livewireScripts is present in your layout — this is what actually loads Livewire's bundled Alpine, so removing your own Alpine without this present would break Alpine entirely.
  5. Rebuild assets with npm run dev or npm run build and hard-refresh the browser.
  6. Open the console and confirm the "already initialized" warning is gone, then click through your Alpine-driven UI (dropdowns, modals, toggles) to confirm everything responds exactly once per interaction.
⚠ Edge case: if you have duplicate wire:key values on nested Livewire components inside a loop, the resulting DOM confusion can sometimes look identical to a duplicate-Alpine-instance bug — components appearing to swap state, or interactions firing on the wrong element. Rule out missing or duplicate wire:key values first before assuming Alpine is the culprit; the two issues produce overlapping symptoms but need completely different fixes.

Still seeing double-init warnings after removing every extra Alpine source? Share your app.js and layout file on the Contact page and I'll help spot the leftover import.

Advertisement
Advertisement