12k
All articles

Créer des applications interactives avec Phoenix LiveView

Créez une recherche avec Phoenix LiveView, un état côté serveur, des modèles HEEx, des événements et un hook JavaScript intégré. Découvrez ses limites.

OpenReplay Team
OpenReplay Team
Créer des applications interactives avec Phoenix LiveView

Phoenix LiveView effectue le rendu HTML côté serveur, conserve l’état de chaque page dans un processus Elixir de longue durée et n’envoie au navigateur que les parties modifiées de la page, via une connexion persistante.

Si vous développez des écrans CRUD en React ou en Vue, vous connaissez la routine : un endpoint JSON, un store côté client, des états de chargement et une validation écrite deux fois, une de chaque côté. L’essentiel de ce code ne sert qu’à faire transiter des données entre deux copies du même état.

Cet article montre comment LiveView supprime la couche API pour ce type d’application. Vous allez construire un champ de recherche en temps réel avec la syntaxe de LiveView 1.2, y ajouter un peu de JavaScript côté client, puis voir où ce modèle atteint ses limites.

Points clés

  • Une LiveView conserve l’état de l’interface dans un processus serveur. Les événements du navigateur transitent par une WebSocket, et LiveView ne renvoie que le HTML qui a changé.
  • Le premier rendu d’une LiveView est une simple réponse HTTP, et mount/3 s’exécute de nouveau une fois la WebSocket connectée.
  • Une LiveView de base nécessite trois callbacks : mount/3 définit l’état initial, render/1 renvoie un template HEEx et handle_event/3 met à jour l’état en réponse à des bindings tels que phx-click et phx-change.
  • Les hooks colocalisés placent le JavaScript d’un composant dans une balise <script> à côté de son balisage. Ils nécessitent Phoenix 1.8 ou une version ultérieure.
  • Chaque événement traité côté serveur implique un aller-retour réseau. React ou Vue restent plus adaptés aux applications offline-first et aux interfaces à forte interaction côté client.

Que fait Phoenix LiveView ?

Phoenix LiveView déplace l’état, la gestion des événements et le rendu du navigateur vers le serveur, tandis que le navigateur maintient une connexion active pour recevoir les mises à jour. La documentation de LiveView décrit chaque LiveView comme un processus : il reçoit des événements, modifie son état et n’envoie à la page que les parties qui diffèrent.

Transposé au modèle SPA que vous connaissez déjà :

Concept SPAÉquivalent LiveView
État du composant (useState, ref)Assigns sur le socket côté serveur
Gestionnaire d’événements (onClick)Callback handle_event/3
Patch du DOM virtuelDiff HTML poussé via la WebSocket
Appel API pour récupérer des donnéesUn appel de fonction au sein du même processus

Le cycle de vie se déroule en deux phases. Le premier rendu est une réponse HTTP classique : la page est donc visible et indexable avant même le chargement du moindre JavaScript. Le navigateur ouvre ensuite une WebSocket, et mount/3 s’exécute une seconde fois dans un processus qui reste actif tant que la page est ouverte. Le code de mount/3 s’exécute donc deux fois à chaque chargement de page.

Si vous avez déjà utilisé htmx, le principe consistant à piloter l’interface avec du HTML rendu côté serveur vous semblera familier. La différence, c’est que LiveView maintient en plus un processus avec état côté serveur pour chaque page connectée.

Comment créer un projet Phoenix et ajouter une route live ?

Une nouvelle application Phoenix 1.8 est livrée avec LiveView déjà configuré : une seule commande de génération suffit donc pour obtenir tout ce dont cet exemple a besoin. Après avoir installé Elixir et l’installeur Phoenix, exécutez :

mix phx.new my_app --no-ecto
cd my_app
mix phx.server

L’option --no-ecto fait l’impasse sur la couche base de données, inutile pour une démo. Le serveur de développement écoute sur http://localhost:4000. Inutile d’ajouter l’option --live : les anciens tutoriels la mentionnent, mais LiveView fait désormais partie de toute application générée.

Ouvrez lib/my_app_web/router.ex. Dans le bloc existant scope "/", MyAppWeb do, sous pipe_through :browser, ajoutez :

live "/search", SearchLive

La macro live associe un chemin à un module LiveView, de la même manière que get associe un chemin à un contrôleur. Remplacez MyApp par le nom de votre application partout où il apparaît. Phoenix fournit d’emblée des générateurs, un scaffolding d’authentification et LiveView, ce qui en fait un framework « batteries incluses ».

Comment créer un champ de recherche en temps réel avec Phoenix LiveView ?

Un champ de recherche en temps réel nécessite un binding de formulaire qui réagit à la saisie et un binding de clic qui réinitialise l’état. Créez lib/my_app_web/live/search_live.ex :

defmodule MyAppWeb.SearchLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, query: "", results: frameworks())}
  end

  def render(assigns) do
    ~H"""
    <form phx-change="search" phx-submit="search">
      <input
        id="search-input"
        type="text"
        name="q"
        value={@query}
        placeholder="Filter frameworks"
        autocomplete="off"
        phx-debounce="300"
      />
    </form>
    <button phx-click="clear">Clear</button>
    <p>{length(@results)} matches</p>
    <ul>
      <li :for={name <- @results}>{name}</li>
    </ul>
    """
  end

  def handle_event("search", %{"q" => q}, socket) do
    {:noreply,
     socket
     |> assign(:query, q)
     |> assign(:results, filter(q))}
  end

  def handle_event("clear", _params, socket) do
    {:noreply, assign(socket, query: "", results: frameworks())}
  end

  defp frameworks do
    ["Phoenix", "Rails", "Laravel", "Django", "Next.js", "Nuxt", "SvelteKit", "Remix"]
  end

  defp filter(q) do
    needle = String.downcase(q)
    Enum.filter(frameworks(), &String.contains?(String.downcase(&1), needle))
  end
end

mount/3 reçoit les paramètres, la session et le socket, dans cet ordre, et renvoie le socket avec les assigns initiaux. render/1 renvoie un template HEEx construit avec le sigil ~H. Chacun des bindings phx-change, phx-submit et phx-click envoie un événement nommé à handle_event/3, qui filtre sur le nom de l’événement, renvoie {:noreply, socket} avec les assigns mis à jour et laisse LiveView pousser un diff du HTML concerné.

phx-change se déclenche pendant la saisie de l’utilisateur : chaque frappe entraînerait donc un aller-retour. phx-debounce en limite la fréquence et accepte une durée en millisecondes ou "blur". Faire pointer phx-submit vers le même événement permet à LiveView de gérer la touche Entrée au lieu de laisser le navigateur soumettre le formulaire. Le guide des bindings de formulaire montre que les paramètres du formulaire arrivent sous forme de map à clés de type chaîne, nommées d’après les champs, par exemple %{"q" => "pho"}.

Les templates interpolent les valeurs avec {@query}. Les tutoriels qui utilisent <%= @query %> sont antérieurs à la syntaxe actuelle. L’attribut :for est le raccourci HEEx pour une compréhension.

Les notions d’Elixir nécessaires pour ce fichier :

  • Atomes : :ok, :noreply ou :query sont des constantes nommées, comparables à des enums de chaînes.
  • Tuples : {:noreply, socket} est un groupe de valeurs de taille fixe. Les callbacks les renvoient pour indiquer ce qui s’est passé.
  • Têtes de fonction avec pattern matching : les deux clauses handle_event/3 sont des définitions distinctes, et Elixir exécute celle dont le premier argument correspond au nom de l’événement. %{"q" => q} déstructure les paramètres comme le ferait const { q } = params en JavaScript.
  • Pipes : |> transmet la valeur située à sa gauche comme premier argument de l’appel suivant.
  • Immutabilité : assign renvoie un nouveau socket. Rien n’est modifié en place.

Comment ajouter du JavaScript côté client avec un hook colocalisé ?

Un hook colocalisé permet d’écrire le JavaScript d’un composant dans une balise <script> placée juste à côté de son balisage. Le nom du hook commence par un point, et l’élément auquel il est rattaché doit posséder un id DOM unique. Le guide d’interopérabilité JS détaille l’API complète des hooks.

L’exemple ci-dessous ajoute des raccourcis clavier : / place le focus sur le champ de recherche et Échap le vide. Remplacez l’<input> de render/1 par :

<input id="search-input" type="text" name="q" value={@query}
  placeholder="Filter frameworks" autocomplete="off"
  phx-debounce="300"
  phx-hook=".SearchShortcuts" />
<script :type={Phoenix.LiveView.ColocatedHook} name=".SearchShortcuts">
  export default {
    mounted() {
      this.onKey = (e) => {
        if (e.key === "/" && document.activeElement !== this.el) {
          e.preventDefault()
          this.el.focus()
        }
        if (e.key === "Escape" && document.activeElement === this.el) {
          this.el.value = ""
          this.el.blur()
          this.pushEvent("clear", {})
        }
      }
      window.addEventListener("keydown", this.onKey)
    },
    destroyed() {
      window.removeEventListener("keydown", this.onKey)
    }
  }
</script>

mounted s’exécute lorsque l’élément est ajouté à la page et destroyed lorsqu’il en est retiré : l’écouteur est donc attaché une seule fois, puis nettoyé. pushEvent("clear", {}) déclenche la même clause handle_event("clear", ...) que celle utilisée par le bouton, si bien que client et serveur partagent un seul et même chemin de code. Vider le champ et lui retirer le focus avant de pousser l’événement permet de garder la valeur affichée synchronisée avec l’état côté serveur.

Le module ColocatedHook extrait le script à la compilation. Un nom préfixé par un point est placé dans l’espace de noms du module, et la fonctionnalité nécessite Phoenix 1.8 ou une version ultérieure. Le fichier app.js généré par mix phx.new enregistre déjà ces hooks :

import {hooks as colocatedHooks} from "phoenix-colocated/my_app"
// ...
hooks: {...colocatedHooks}

Quand LiveView est-il adapté, et quand ne l’est-il pas ?

Chaque événement traité par handle_event/3 implique un aller-retour avec le serveur. Une LiveView paraît instantanée sur un réseau rapide et poussive sur un réseau lent. Les changements purement visuels, comme l’ouverture d’un menu déroulant, peuvent s’exécuter dans le navigateur grâce aux commandes Phoenix.LiveView.JS, sans jamais solliciter le serveur.

En local, cette latence passe inaperçue. Le fichier assets/js/app.js généré par mix phx.new expose déjà le socket via window.liveSocket : pour simuler une connexion lente, exécutez ceci dans la console du navigateur :

liveSocket.enableLatencySim(500)
// later
liveSocket.disableLatencySim()

Le simulateur de latence ajoute le nombre de millisecondes indiqué comme latence unidirectionnelle vers et depuis le serveur. En production, les replays de session d’applications LiveView sur des connexions lentes révèlent le même problème du point de vue de l’utilisateur : un affichage qui accuse un retard sur la frappe, et un clignotement lorsque la page se déconnecte puis se reconnecte.

LiveView présente deux autres inconvénients. Le support hors ligne est difficile à mettre en place, car les événements nécessitent une connexion active au serveur. Vous devez en outre faire tourner Elixir en production, avec un processus par page connectée.

SituationChoix le plus adaptéPourquoi
Écrans CRUD, tableaux de bord, formulaires, outils d’administrationLiveViewPas de couche API distincte ; l’état et le rendu restent sur le serveur
Applications offline-first ou à faible connectivitéReact / VueLiveView a besoin d’une connexion active pour traiter les événements
Forte interaction côté client (éditeurs, canvas, interfaces riches en glisser-déposer)React / VueChaque interaction coûte un aller-retour ; les hooks aident, mais ne remplacent pas un framework client
Équipe peu encline à adopter ElixirReact / VueVous devez exploiter et maintenir Elixir côté serveur

Pour une vue plus large du compromis entre interfaces pilotées par le serveur et interfaces pilotées par le client, consultez cette comparaison entre htmx, Vue et React.

Conclusion

LiveView convient parfaitement aux applications de type CRUD qui nécessiteraient autrement une API JSON et un store côté client. Vous écrivez vos callbacks dans un seul module, le serveur détient l’état, et les hooks ou les commandes JS prennent en charge ce qui doit s’exécuter dans le navigateur. Prochaine étape conseillée : générez une application Phoenix 1.8, reconstruisez sous forme de LiveView l’un des formulaires de votre SPA actuelle, puis activez le simulateur de latence pour observer son comportement sur une connexion lente avant de vous engager.

FAQ

Que se passe-t-il lorsqu'une LiveView perd sa connexion WebSocket ?

Le client se reconnecte automatiquement avec un back-off exponentiel, et mount/3 s'exécute de nouveau : tous les assigns qui ne sont pas reconstruits dans mount/3 sont donc perdus. Les formulaires qui possèdent à la fois un attribut id et un binding phx-change sont restaurés automatiquement : après la reconnexion, LiveView renvoie les valeurs actuelles du formulaire via l'événement phx-change. Ajoutez phx-auto-recover pour déclencher un autre événement de restauration, ou définissez-le à 'ignore' pour désactiver la restauration.

Comment empêcher le code de mount/3 de s'exécuter deux fois ?

Encapsulez-le dans une vérification connected?(socket). Phoenix.LiveView.connected?/1 renvoie false pendant le rendu HTTP statique et true une fois la WebSocket connectée. Les abonnements, les timers et les requêtes coûteuses placés dans un bloc if connected?(socket) ne s'exécutent donc que dans le processus live. Laissez les assigns par défaut peu coûteux en dehors de cette vérification afin que le rendu statique produise toujours un HTML complet et indexable.

Comment tester une LiveView sans navigateur ?

Utilisez Phoenix.LiveViewTest, fourni avec LiveView et exécuté sous ExUnit. Appelez live(conn, path) pour monter la vue et obtenir un handle, puis pilotez-la avec des helpers tels que render_click et render_change, et vérifiez le HTML qu'ils renvoient. Ces tests communiquent directement avec le processus LiveView : le JavaScript des hooks côté client ne s'y exécute donc pas.

Quelle est la différence entre un composant fonctionnel et un LiveComponent ?

Un composant fonctionnel est une fonction sans état qui reçoit des assigns et renvoie du HEEx : les événements issus de son balisage sont donc envoyés à la LiveView parente. Un LiveComponent, défini avec use Phoenix.LiveComponent, conserve son propre état et gère ses propres événements lorsque son balisage définit phx-target={@myself}. Tous deux s'exécutent dans le processus de la LiveView parente. Privilégiez les composants fonctionnels par défaut, et n'optez pour un LiveComponent que si l'état et la gestion des événements doivent être encapsulés dans le composant.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.