Aller au contenu principal

Module 3 – Data binding et interactions

Niveau 5.1 – Laravel Livewire


Les modificateurs (debounce, lazy, defer) évitent les requêtes excessives ; ce module précise quand chaque valeur est envoyée au serveur et comment faire communiquer les composants (événements). En fin de module : entangle Alpine/Livewire, événements globaux, listes filtrées.


Objectif

À la fin de ce module, vous saurez :

  • Contrôler quand les valeurs des champs sont envoyées au serveur : wire:model par défaut, .lazy, .debounce, .defer, et .live (Livewire 3).
  • Réduire le nombre de requêtes sur les champs de recherche ou les filtres pour de meilleures performances et une meilleure UX.
  • Utiliser les événements Livewire : $dispatch pour qu’un composant enfant notifie le parent (ou d’autres composants), et #[On] pour écouter un événement.
  • Mettre en place un système de filtres dynamiques (recherche, statut, nombre par page) avec binding et requêtes Eloquent dans render() ou une méthode dédiée.

Ce module est essentiel pour maîtriser les performances : sans debounce, une recherche en temps réel enverrait une requête à chaque touche ; sans wire:target, tous les loadings s’afficheraient en même temps. On détaille le flux (quand la valeur part au serveur), les modificateurs, les événements, et un exemple complet de liste filtrée.


Flux : quand la valeur est envoyée au serveur

Dès que vous utilisez wire:model sur un champ, la valeur du champ est synchronisée avec une propriété publique du composant. La question est : à quel moment cette valeur est-elle envoyée au serveur ?

  • Sans modificateur (ou .live) : à chaque changement (chaque frappe pour un input texte, chaque clic pour une checkbox). Le serveur reçoit la requête, met à jour la propriété, appelle updated()* si défini, puis render(). La vue est re-rendue et le DOM mis à jour. Pour un champ de recherche, cela peut signifier dizaines de requêtes en quelques secondes.
  • .lazy : la valeur n’est envoyée que lorsque le champ perd le focus (blur). Une seule requête quand l’utilisateur quitte le champ.
  • .debounce.300ms : après chaque frappe, Livewire attend 300 ms. Si une nouvelle frappe arrive avant la fin des 300 ms, le délai est réinitialisé. La requête part seulement 300 ms après la dernière frappe. Idéal pour la recherche.
  • .defer : la valeur reste locale jusqu’à ce qu’une action soit déclenchée (wire:click ou wire:submit). À ce moment, tout l’état (y compris les champs en defer) est envoyé. Aucune requête tant que l’utilisateur n’a pas cliqué sur « Appliquer » ou « Rechercher ».

Choisir le bon modificateur selon le cas : recherche → debounce ; formulaire multi-champs → defer ou lazy ; case à cocher ou select qui doit mettre à jour la liste tout de suite → .live.


Modes de binding : quand la valeur est envoyée au serveur

Avec wire:model seul, Livewire 3 envoie les mises à jour en direct (à chaque changement) par défaut. Pour des champs de recherche ou des filtres, cela peut générer une requête à chaque frappe, ce qui est coûteux et souvent inutile. Les modificateurs permettent de retarder ou regrouper les envois.

ModificateurComportementCas d’usage
wire:model (ou .live)Mise à jour à chaque changement (chaque frappe pour un input texte).Champs où la réactivité immédiate est nécessaire (ex. compteur, case à cocher).
.lazyMise à jour au blur (quand le champ perd le focus).Champs où on ne veut pas envoyer à chaque frappe (nom, email) ; une requête au moment de quitter le champ.
.debounce.500msAttente de 500 ms après la dernière frappe avant d’envoyer.Recherche en temps réel : l’utilisateur tape, on envoie après une courte pause.
.deferLa valeur n’est pas envoyée à chaque changement ; elle est envoyée uniquement lors d’une action (ex. wire:click, wire:submit).Formulaires avec plusieurs champs : on envoie tout en une fois au clic sur « Appliquer » ou « Rechercher ».

Exemples :

<input wire:model="search" placeholder="Recherche à chaque frappe">
<input wire:model.lazy="email" placeholder="Mis à jour au blur">
<input wire:model.live.debounce.300ms="search" placeholder="Recherche avec debounce 300 ms">
<input wire:model.defer="category" placeholder="Envoyé seulement au submit ou au clic">
  • wire:model.live.debounce.300ms="search" : idéal pour une barre de recherche. L’utilisateur tape ; après 300 ms sans frappe, une seule requête part avec la valeur actuelle.
  • wire:model.defer sur plusieurs champs + un bouton wire:click="applyFilters" : les valeurs ne sont envoyées qu’au clic, ce qui limite les requêtes et garde un comportement prévisible.

Événements : $dispatch et listeners

Parfois un composant enfant doit informer un composant parent (ou un autre composant) qu’une action s’est produite, sans couplage direct (pas de passage de callbacks en props). Livewire propose un système d’événements :

  • Émettre : dans la classe PHP, $this->dispatch('nom-evenement', param1: $valeur1, param2: $valeur2);. L’événement est émis côté serveur et peut être écouté par d’autres composants (parent, frères, ou globalement).
  • Écouter : dans une classe, vous déclarez une méthode qui réagit à un événement avec l’attribut #[On('nom-evenement')] (Livewire 3). Les paramètres de l’événement sont passés en arguments de la méthode.

Exemple : enfant notifie le parent

Composant enfant (ex. un formulaire de création) :

use Livewire\Component;

class CreateUserForm extends Component
{
public function save(): void
{
// ... création utilisateur ...
$this->dispatch('user-created', userId: $user->id);
}

public function render()
{
return view('livewire.create-user-form');
}
}

Composant parent (ex. page qui contient la liste et le formulaire) :

use Livewire\Component;
use Livewire\Attributes\On;

class UserManager extends Component
{
#[On('user-created')]
public function refreshList(int $userId): void
{
// Rafraîchir la liste ou afficher un message
session()->flash('message', 'Utilisateur #' . $userId . ' créé.');
}

public function render()
{
return view('livewire.user-manager');
}
}
  • $this->dispatch('user-created', userId: $user->id) : émet l’événement avec un paramètre userId.
  • #[On('user-created')] : la méthode refreshList est appelée quand l’événement user-created est reçu. Le paramètre nommé userId est passé en argument (int $userId).

Cela permet de garder les composants découplés : l’enfant n’a pas besoin de connaître le parent ; il émet un événement. Le parent (ou tout autre composant qui écoute) réagit comme il le souhaite.


Exemple complet : liste d’articles avec filtres dynamiques

Nous construisons un composant ArticleList avec :

  • Recherche par titre (wire:model avec debounce).
  • Statut (select : tous, publié, brouillon).
  • Nombre par page (select : 5, 10, 25).
  • Les articles sont chargés via Eloquent dans render() en fonction de $search, $status et $perPage.

Classe app/Livewire/ArticleList.php

<?php

namespace App\Livewire;

use App\Models\Article;
use Livewire\Component;
use Livewire\WithPagination;

class ArticleList extends Component
{
use WithPagination;

public string $search = '';
public string $status = '';
public int $perPage = 10;

public function render()
{
$articles = Article::query()
->when($this->search, function ($q) {
$q->where('title', 'like', '%' . $this->search . '%');
})
->when($this->status !== '', function ($q) {
$q->where('status', $this->status);
})
->orderBy('created_at', 'desc')
->paginate($this->perPage);

return view('livewire.article-list', [
'articles' => $articles,
]);
}
}
  • WithPagination : trait Livewire pour la pagination (voir module 6). paginate($this->perPage) renvoie une instance LengthAwarePaginator.
  • when($this->search, ...) : n’applique le filtre que si $search n’est pas vide. Idem pour $status.
  • À chaque changement de $search, $status ou $perPage (via wire:model), render() est rappelé et la liste est mise à jour.

Vue resources/views/livewire/article-list.blade.php

<div>
<div class="filters flex gap-4 mb-4">
<input type="text"
wire:model.live.debounce.300ms="search"
placeholder="Rechercher par titre...">
<select wire:model.live="status">
<option value="">Tous les statuts</option>
<option value="published">Publié</option>
<option value="draft">Brouillon</option>
</select>
<select wire:model.live="perPage">
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
</select>
</div>

<ul>
@foreach ($articles as $article)
<li>{{ $article->title }} ({{ $article->status }})</li>
@endforeach
</ul>

{{ $articles->links() }}
</div>
  • wire:model.live.debounce.300ms="search" : la recherche envoie une requête 300 ms après la dernière frappe.
  • wire:model.live="status" et wire:model.live="perPage" : dès que l’utilisateur change le select, une requête part et la liste se met à jour.
  • $articles->links() : liens de pagination Livewire (nécessite WithPagination dans la classe).

Variante avec .defer : si vous préférez que les filtres ne s’appliquent qu’au clic sur un bouton « Appliquer », mettez wire:model.defer sur les trois champs et ajoutez un bouton wire:click="applyFilters". Dans applyFilters(), appelez $this->resetPage() pour revenir à la page 1 (sinon vous pouvez rester sur la page 5 avec un filtre qui ne renvoie que 2 pages). Le simple fait de déclencher une action envoie l’état à jour (y compris les champs en defer).


Pièges à éviter

  • Recherche sans debounce : wire:model.live seul sur un champ de recherche = une requête à chaque frappe. L’utilisateur tape « Laravel » et vous envoyez 7 requêtes. Toujours utiliser wire:model.live.debounce.300ms (ou 500ms) pour la recherche.
  • Oublier resetPage() : quand vous changez $search, $status ou $perPage, la pagination peut rester sur la page 5 alors que les nouveaux résultats ne font que 2 pages. Dans updatedSearch(), updatedStatus(), etc., appelez $this->resetPage() (module 6).
  • Événements : nom et paramètres : le nom de l’événement doit être identique entre dispatch() et #[On()]. Les paramètres nommés (userId: $id) sont passés en arguments de la méthode listener ; les types doivent correspondre (ex. int $userId).

À retenir

  • Quand la valeur part : .live = à chaque changement ; .lazy = au blur ; .debounce.300ms = 300 ms après la dernière frappe ; .defer = uniquement à la prochaine action (clic, submit).
  • wire:model.live.debounce.300ms sur la recherche pour limiter les requêtes ; wire:model.defer sur les filtres + bouton « Appliquer » pour tout envoyer en une fois.
  • $this->dispatch('nom-evenement', param: $valeur) pour émettre ; #[On('nom-evenement')] sur une méthode pour écouter. Communication découplée entre composants.
  • Listes filtrées : requête dans render() avec when($this->search, ...) ; WithPagination et $articles->links() ; resetPage() dans updated()* quand les filtres changent (module 6).

Choix du modificateur : guide rapide

SituationModificateur conseilléRaison
Champ de recherche (liste qui se met à jour)wire:model.live.debounce.300msUne requête 300 ms après la dernière frappe ; évite une requête par touche.
Champ nom, email (formulaire classique)wire:model.lazy ou wire:model.deferLazy = une requête au blur ; defer = envoi uniquement au submit.
Select (filtre statut, catégorie)wire:model.liveUne requête à chaque changement ; l’utilisateur s’attend à une mise à jour immédiate.
Checkbox ou radiowire:model ou .liveSouvent une seule interaction ; pas besoin de debounce.
Formulaire avec bouton « Appliquer »wire:model.defer sur tous les champsUne seule requête au clic ; comportement prévisible.

Approfondissement

  • Événements globaux : $this->dispatch('...') émet vers les composants parents et les frères par défaut. Pour écouter depuis n’importe où (composant hors arbre parent), utilisez le nom d’événement avec le préfixe ou la config Livewire pour les événements globaux (voir doc officielle Livewire 3). Utile pour une barre de notification ou un panier mis à jour depuis n’importe quelle page.
  • Entangle (Alpine + Livewire) : en Livewire 3 avec Alpine, vous pouvez synchroniser une variable Alpine avec une propriété Livewire pour qu’un comportement côté client (ex. ouverture d’un modal) et l’état serveur restent alignés. Avancé ; à réserver aux cas où vous mélangez beaucoup d’Alpine et de Livewire sur le même composant.
  • Liste filtrée très longue : si render() devient lent (grosse requête, beaucoup de lignes), envisagez de limiter les colonnes (select()), d’indexer les colonnes de filtre/tri en BDD, et éventuellement d’utiliser wire:model.defer sur tous les filtres + un bouton « Appliquer » pour ne déclencher qu’une seule requête lourde au clic.
  • Tests : les événements $dispatch peuvent être assertés dans les tests Livewire (module 11) pour vérifier qu’un composant enfant notifie bien le parent après une action.

À retenir

  • Quand la valeur part : .live = à chaque changement ; .lazy = au blur ; .debounce.300ms = 300 ms après la dernière frappe ; .defer = uniquement à la prochaine action (clic, submit).
  • wire:model.live.debounce.300ms sur la recherche ; wire:model.defer + bouton « Appliquer » pour les filtres multiples ; $this->dispatch() et #[On()] pour la communication entre composants.
  • resetPage() dans updatedSearch() / updatedStatus() pour ne pas rester sur une page vide après changement de filtre (module 6).

Dans le prochain module, nous voyons les formulaires robustes : validation avec #[Rule], validate() et validateOnly(), validation temps réel, et upload de fichiers avec WithFileUploads.