Після кожного запиту Livewire не замінює HTML компонента повністю, а морфить DOM: порівнює старе й нове дерево елемент за елементом і змінює лише відмінності. Так зберігаються фокус, введений текст, стан Alpine.
Проблема зі списками. Без підказок морфінг зіставляє елементи за позицією. Видалили перший елемент списку - і алгоритм вирішує, що змінився вміст кожного елемента, а останній зник:
@foreach ($todos as $todo)
<li>
<input type="checkbox" wire:model="done.{{ $todo->id }}">
<span x-data="{ editing: false }">{{ $todo->title }}</span>
</li>
@endforeach
Наслідки: стан Alpine (editing), фокус і введення «переїжджають» на сусідні рядки, анімації спрацьовують не там, а вкладені Livewire-компоненти отримують чужі дані.
wire:key дає кожному елементу стабільний ідентифікатор - морфінг зіставляє елементи за ключем:
@foreach ($todos as $todo)
<li wire:key="todo-{{ $todo->id }}">...</li>
@endforeach
@foreach ($posts as $post)
<livewire:post-card :$post :wire:key="$post->id" />
@endforeach
Правила:
- ключ - ідентифікатор даних (
id), а не індекс циклу.$loop->indexне допомагає: після видалення елемента індекси зсуваються так само; - ключ має бути унікальним у межах компонента. Якщо на сторінці два списки з тих самих моделей, додайте префікс:
"comment-{{ $id }}","reply-{{ $id }}"; - для вкладених Livewire-компонентів у циклі ключ обов'язковий - без нього стан дочірніх компонентів змішується.
Налаштування smart_wire_keys (у Livewire 4 увімкнено за замовчуванням) допомагає з ключами глибоко вкладених компонентів, але не звільняє від wire:key у циклах.
Ще один випадок - залежні поля: <select> міст, що змінюється разом з обраною областю, потребує wire:key="{{ $regionId }}", щоб значення коректно скинулося.
Альтернатива морфінгу для складних випадків - wire:replace: замінити вміст елемента повністю замість порівняння.