Laravel Livewire/AlpineJS: Disable a button while loading data from an API and then enabling it again

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

Laravel Livewire/AlpineJS: Mastering Asynchronous State Management for UI Feedback

Dealing with asynchronous operations—like fetching data from an API—and providing appropriate user feedback (disabling buttons, showing spinners) is a fundamental aspect of modern web development. When integrating powerful frameworks like Laravel Livewire and Alpine.js, managing this state synchronization across the server (PHP) and the client (JavaScript) can sometimes feel counter-intuitive.

You are running into a very common synchronization pitfall when mixing Livewire's reactive properties with client-side directives like Alpine.js. While Livewire excels at keeping the server and client in sync, ensuring that a specific state change triggers the necessary UI update requires careful attention to how data flows between the component logic and the view bindings.

Let’s break down exactly why your button isn't re-enabling itself and how to fix this robustly.

The Synchronization Challenge: Livewire State vs. AlpineJS Bindings

Your setup correctly identifies the core components: a server-side property ($loading in the Livewire component) that is synchronized with a client-side state (loading in AlpineJS). The issue usually lies in how the asynchronous operation resolves and forces a full re-render across both layers.

When you call $this->loading = false; inside your loader() method, Livewire updates the DOM based on the component's new state. However, if the client-side binding isn't correctly listening for this specific state change at the exact moment of completion, the visual feedback remains stuck.

The key is to ensure that any property you use in an @entangle or x-bind directive is explicitly updated after the asynchronous work has finished and before Livewire commits the final view.

The Solution: Leveraging Livewire's Reaction Cycle

In most scenarios involving standard Livewire methods, simply setting the state variable inside the method should be enough for a subsequent render. If it isn't working as expected, we need to ensure that the entire flow is treated as a single, atomic update triggered by the component lifecycle.

For operations that involve waiting, always treat the state change as the final trigger for the UI update. Since Livewire manages the view updates based on property changes, ensuring that $this->loading reflects the true completion status is paramount.

Here is how we refine your pattern to ensure perfect synchronization:

Refined Implementation Example

We will keep the structure similar but focus on the data flow:

1. The Livewire Component (YourComponent.php)

The logic remains sound; the change needs to be correctly reflected upon exit.

php
class YourComponent extends Component
{
    public bool $loading = false;

    public function loader()
    {
        $this->loading = true; // Set loading state immediately before API call
        
        // Simulate a long-running API call (e.g., 10 seconds)
        sleep(10); 
        
        // Perform API fetching and processing here...

        $this->loading = false; // Crucial: Set loading to false when done
    }
}

2. The Blade View (your-component.blade.php)

The AlpineJS binding correctly tracks the Livewire property using @entangle.

html
<div>
    <div class="grid grid-cols-3 gap-4">
        {{-- Ensure x-data reflects the live state --}}
        <div 
            class="p-4 border"
            x-data="{ loading: @entangle('loading') }"
        >
            <button
                type="button"
                class="btn btn-sm btn-jbn"
                x-on:click="loading = true"
                x-bind:disabled="loading"
                wire:click="loader"
            >
                Load API
            </button>
        </div>
    </div>
</div>

Why This Works (and What You Were Missing)

The reason this pattern works perfectly, and why you might have encountered issues, often relates to when the state is updated relative to the execution flow. In Livewire, when a method finishes executing, it triggers a full reconciliation of the component's properties reflected in the view. By explicitly setting $this->loading = false; as the very last step within your method, you are telling Livewire that the component state has finalized for this request.

If you were experiencing delays or flickering, it often points toward an issue with how AlpineJS is interpreting the @entangle mechanism during a very long server-side wait. By ensuring the synchronous update of the primary property ($this->loading) is the final action in your Livewire method, you guarantee that the client receives the correct signal for disabling and re-enabling the button precisely when the data operation concludes.

Conclusion: Rely on Livewire for State Authority

When building complex interactive UIs with Laravel, the golden rule is to let the reactive framework—in this case, Livewire—be the single source of truth for state management. While Alpine.js provides excellent declarative control over local UI interactions (like toggling modals or handling input focus), it should rely on the server-side data provided by Livewire.

By ensuring that all critical loading states are managed exclusively within the Livewire component and correctly exposed via @entangle, you establish a robust, reliable synchronization pipeline. Always aim to let the backend handle the heavy lifting of state transitions; the frontend merely reflects what the backend tells it. For more deep dives into state management patterns in Laravel, exploring the official documentation at laravelcompany.com is highly recommended.