Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

lucasweaver's avatar

Can't get data to connect after filtering collection - Alpine + Livewire + $loop->iteration

I'm trying to load a set of $flashcards, and then after the user changes a filter to include learned cards, fetch the new group of $flashcards, and then show them one by one if they match the $loop->iteration.

<div x-data="{ card: $wire.entangle('card'), totalCards: {{ count($flashcards) }} }">
    <div class="bg-gray-900 mb-4 min-h-screen min-w-screen pt-3 px-5 flex justify-center"> 
        @foreach($flashcards as $flashcard)        
        <div x-show="card == '{{ $loop->iteration }}' " wire:key="flashcard-{{ $flashcard->id }}">
            <div x-text="card" class="p-3 bg-red-300 text-white"></div>
             <div class="p-3 bg-blue-400 text-white">{{ $loop->iteration}}</div>            
            <div class="bg-gray-700 text-white">
            {{ $flashcard->term }}
            </div>
        </div>
        @endforeach
        <div>            
            <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='nextCard'>Next Card</button>
            <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='toggleLearned'>Toggle Learned</button>
        </div>
    </div>
</div>
<?php

namespace App\Livewire\Flashcards;

use Livewire\Component;
use App\Models\FlashcardSet;

class Study extends Component
{
    public $flashcardSetId;
    public $flashcards;
    public $includeLearned = true;
    public $card = 1;
    
    public function toggleLearned()
    {
        $this->includeLearned = !$this->includeLearned;
        $this->getFlashcards();
        $this->resetCard();
        $this->dispatch('flashcardsUpdated');     
    }

    public function resetCard()
    {
        $this->card = 1;        
    }

    public function nextCard()
    {
        $this->card++;
    }
    
    public function getFlashcards()
    {        
        if(!$this->includeLearned)
        {
            $flashcardSet = FlashcardSet::find($this->flashcardSetId);
            $user_id = auth()->id();
            $this->flashcards = $flashcardSet->flashcards()
                ->where(function ($query) use ($user_id) {
                    $query->doesntHave('userFlashcardProgress')
                        ->orWhereHas('userFlashcardProgress', function ($subQuery) use ($user_id) {
                            $subQuery->where('user_id', $user_id)
                                ->whereNotIn('status', ['learned', 'mastered']);
                        });
                })
                ->get();            
        } else {
            $flashcardSet = FlashcardSet::find($this->flashcardSetId);
            $this->flashcards = $flashcardSet->flashcards()->get();
        }
                
    }

    public function mount($flashcardSetId)
    {
        $this->flashcardSetId = $flashcardSetId;
        $this->getFlashcards();                
    }
    
    public function render()
    {        
        return view('livewire.flashcards.study');
    }
}

I suspected it was because Alpine can't get proper access to the $loop->iteration value after the re-render of the $flashcards. But I haven't been able to find a workaround at all.

Let's say there are 6 cards originally, after the reload, the 4th card (which is learned), won't show. But if I manually add + 1 to the count of "card" in the livewire dev tools, and then decrease one, it will show correctly.

But without that manual change, it won't update at all.

And sometimes after a few toggles, it will show the old 1 and 4 $loop->iteration with the same 1 for the card value.

0 likes
3 replies
LaryAI's avatar
Level 58

To address the issue you're facing with Alpine.js and Livewire, it seems like the problem is related to the synchronization between the Alpine.js state and the Livewire component state. Specifically, Alpine.js might not be aware of the changes in the Livewire component state after re-rendering.

Here's a step-by-step solution to ensure that Alpine.js correctly updates its state based on the Livewire component state:

  1. Initialize Alpine.js State from Livewire: Ensure that Alpine.js initializes its state based on the Livewire component state when the component is first rendered.

  2. Listen for Livewire Events: Use Livewire events to notify Alpine.js about state changes.

  3. Update Alpine.js State: Update the Alpine.js state when Livewire events are triggered.

Here's how you can implement these steps:

Step 1: Initialize Alpine.js State

Modify your Blade template to initialize the Alpine.js state based on the Livewire component state:

<div class="bg-gray-900 mb-4 min-h-screen min-w-screen pt-3 px-5 flex justify-center" x-data="{ card: @entangle('card') }">
    @foreach($flashcards as $flashcard)
    <div x-show="card == '{{ $loop->iteration }}'" wire:key="flashcard-{{ $flashcard->id }}">
        <div x-text="card" class="p-3 bg-red-300 text-white"></div>
        <div class="p-3 bg-blue-400 text-white">{{ $loop->iteration }}</div>
        <div class="bg-gray-700 text-white">
            {{ $flashcard->term }}
        </div>
    </div>
    @endforeach
    <div>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='nextCard'>Next Card</button>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='toggleLearned'>Toggle Learned</button>
    </div>
</div>

Step 2: Listen for Livewire Events

In your Livewire component, dispatch an event when the flashcards are updated:

public function toggleLearned()
{
    $this->includeLearned = !$this->includeLearned;
    $this->getFlashcards();
    $this->resetCard();
    $this->dispatchBrowserEvent('flashcardsUpdated');
}

Step 3: Update Alpine.js State

In your Blade template, listen for the flashcardsUpdated event and update the Alpine.js state accordingly:

<div class="bg-gray-900 mb-4 min-h-screen min-w-screen pt-3 px-5 flex justify-center" x-data="{ card: @entangle('card') }" @flashcardsUpdated.window="card = @entangle('card')">
    @foreach($flashcards as $flashcard)
    <div x-show="card == '{{ $loop->iteration }}'" wire:key="flashcard-{{ $flashcard->id }}">
        <div x-text="card" class="p-3 bg-red-300 text-white"></div>
        <div class="p-3 bg-blue-400 text-white">{{ $loop->iteration }}</div>
        <div class="bg-gray-700 text-white">
            {{ $flashcard->term }}
        </div>
    </div>
    @endforeach
    <div>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='nextCard'>Next Card</button>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='toggleLearned'>Toggle Learned</button>
    </div>
</div>

Full Solution Code

Here is the complete code with the above changes:

Blade Template

<div class="bg-gray-900 mb-4 min-h-screen min-w-screen pt-3 px-5 flex justify-center" x-data="{ card: @entangle('card') }" @flashcardsUpdated.window="card = @entangle('card')">
    @foreach($flashcards as $flashcard)
    <div x-show="card == '{{ $loop->iteration }}'" wire:key="flashcard-{{ $flashcard->id }}">
        <div x-text="card" class="p-3 bg-red-300 text-white"></div>
        <div class="p-3 bg-blue-400 text-white">{{ $loop->iteration }}</div>
        <div class="bg-gray-700 text-white">
            {{ $flashcard->term }}
        </div>
    </div>
    @endforeach
    <div>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='nextCard'>Next Card</button>
        <button class="bg-teal-400 text-white px-2 py-2 mt-4" wire:click='toggleLearned'>Toggle Learned</button>
    </div>
</div>

Livewire Component

<?php

namespace App\Livewire\Flashcards;

use Livewire\Component;
use App\Models\FlashcardSet;

class Study extends Component
{
    public $flashcardSetId;
    public $flashcards;
    public $includeLearned = true;
    public $card = 1;
    
    public function toggleLearned()
    {
        $this->includeLearned = !$this->includeLearned;
        $this->getFlashcards();
        $this->resetCard();
        $this->dispatchBrowserEvent('flashcardsUpdated');
    }

    public function resetCard()
    {
        $this->card = 1;        
    }

    public function nextCard()
    {
        $this->card++;
    }
    
    public function getFlashcards()
    {        
        if(!$this->includeLearned)
        {
            $flashcardSet = FlashcardSet::find($this->flashcardSetId);
            $user_id = auth()->id();
            $this->flashcards = $flashcardSet->flashcards()
                ->where(function ($query) use ($user_id) {
                    $query->doesntHave('userFlashcardProgress')
                        ->orWhereHas('userFlashcardProgress', function ($subQuery) use ($user_id) {
                            $subQuery->where('user_id', $user_id)
                                ->whereNotIn('status', ['learned', 'mastered']);
                        });
                })
                ->get();            
        } else {
            $flashcardSet = FlashcardSet::find($this->flashcardSetId);
            $this->flashcards = $flashcardSet->flashcards()->get();
        }
                
    }

    public function mount($flashcardSetId)
    {
        $this->flashcardSetId = $flashcardSetId;
        $this->getFlashcards();                
    }
    
    public function render()
    {        
        return view('livewire.flashcards.study');
    }
}

This solution ensures that Alpine.js correctly updates its state based on the Livewire component state after re-rendering.

lucasweaver's avatar

I have stupidly spent the last 10 hours of this with ChatGPT, so sadly Lary's approaches have been tried and haven't worked.

I've tried sending an event to update the 'card' value in alpine, with x-on and with a global Livewire init / initialized listener. All approaches I've found on ChatGPT or in other forums have given me the same result.

Please or to participate in this conversation.