Creación de aplicaciones interactivas con Phoenix LiveView
Crea una búsqueda con Phoenix LiveView, estado en el servidor, plantillas HEEx, eventos y un hook JavaScript integrado. Conoce sus límites y ventajas.
Phoenix LiveView renderiza el HTML en el servidor, mantiene el estado de cada página en un proceso de Elixir de larga duración y envía al navegador solo las partes de la página que cambian, a través de una conexión persistente.
Si desarrollas pantallas CRUD con React o Vue, ya conoces la rutina: un endpoint JSON, un store en el cliente, estados de carga y una validación que se escribe dos veces, una en cada lado. La mayor parte de ese código solo sirve para mover datos entre dos copias del mismo estado.
Este artículo muestra cómo LiveView elimina la capa de API en ese tipo de aplicaciones. Construirás un cuadro de búsqueda en vivo con la sintaxis de LiveView 1.2, añadirás una pieza de JavaScript del lado del cliente y verás en qué casos el modelo deja de encajar.
Puntos clave
- Un LiveView mantiene el estado de la UI en un proceso del servidor. Los eventos del navegador viajan por un WebSocket y LiveView devuelve solo el HTML que ha cambiado.
- El primer renderizado de un LiveView es una respuesta HTTP normal, y
mount/3vuelve a ejecutarse cuando se conecta el WebSocket. - Un LiveView básico necesita tres callbacks:
mount/3establece el estado inicial,render/1devuelve una plantilla HEEx yhandle_event/3actualiza el estado en respuesta a bindings comophx-clickyphx-change. - Los hooks colocados (colocated hooks) sitúan el JavaScript de un componente en una etiqueta
<script>junto a su marcado. Requieren Phoenix 1.8 o posterior. - Cada evento gestionado en el servidor supone un viaje de ida y vuelta por la red. React o Vue siguen siendo la mejor opción para aplicaciones offline-first y para interfaces con mucha interacción en el cliente.
¿Qué hace Phoenix LiveView?
Phoenix LiveView traslada el estado, la gestión de eventos y el renderizado del navegador al servidor, mientras el navegador mantiene abierta una conexión en vivo para recibir actualizaciones. La documentación de LiveView describe cada LiveView como un proceso: recibe eventos, modifica su estado y envía a la página solo las partes que difieren.
Su equivalencia con el modelo SPA que ya conoces:
| Concepto en una SPA | Equivalente en LiveView |
|---|---|
Estado del componente (useState, ref) | Assigns en el socket del lado del servidor |
Manejador de eventos (onClick) | Callback handle_event/3 |
| Parche del Virtual DOM | Diff de HTML enviado por el WebSocket |
| Llamada a la API para obtener datos | Una llamada a función dentro del mismo proceso |
El ciclo de vida transcurre en dos fases. El primer renderizado es una respuesta HTTP ordinaria, de modo que la página es visible e indexable antes de que se cargue cualquier JavaScript. Después, el navegador abre un WebSocket y mount/3 se ejecuta por segunda vez dentro de un proceso que permanece activo mientras la página esté abierta. Por tanto, el código de mount/3 se ejecuta dos veces en cada carga de página.
Si has usado htmx, la idea de controlar la UI con HTML renderizado en el servidor te resultará familiar. La diferencia es que LiveView también mantiene un proceso con estado en el servidor para cada página conectada.
¿Cómo se crea un proyecto Phoenix y se añade una ruta live?
Una nueva aplicación Phoenix 1.8 incluye LiveView ya configurado, así que un único comando del generador te proporciona todo lo que necesita este ejemplo. Después de instalar Elixir y el instalador de Phoenix, ejecuta:
mix phx.new my_app --no-ecto
cd my_app
mix phx.server
El flag --no-ecto omite la capa de base de datos, que una demo no necesita. El servidor de desarrollo escucha en http://localhost:4000. No necesitas el flag --live: los tutoriales antiguos lo usan, pero LiveView ya forma parte de todas las aplicaciones generadas.
Abre lib/my_app_web/router.ex. Dentro del bloque existente scope "/", MyAppWeb do, debajo de pipe_through :browser, añade:
live "/search", SearchLive
La macro live asocia una ruta a un módulo LiveView, de forma muy parecida a como get asocia una ruta a un controlador. Sustituye MyApp por el nombre de tu aplicación allí donde aparezca. Phoenix incluye de serie generadores, scaffolding de autenticación y LiveView, lo que lo convierte en un framework con todo incluido (batteries-included).
¿Cómo se construye un cuadro de búsqueda en vivo con Phoenix LiveView?
Un cuadro de búsqueda en vivo necesita un binding de formulario que reaccione mientras escribes y un binding de clic que restablezca el estado. Crea 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 recibe params, session y socket en ese orden, y devuelve el socket con los assigns iniciales. render/1 devuelve una plantilla HEEx construida con el sigil ~H. Cada uno de los bindings phx-change, phx-submit y phx-click envía un evento con nombre a handle_event/3, que hace pattern matching sobre el nombre del evento, devuelve {:noreply, socket} con los assigns actualizados y deja que LiveView envíe un diff del HTML afectado.
phx-change se dispara mientras el usuario escribe, por lo que cada pulsación de tecla supondría un viaje de ida y vuelta. phx-debounce limita su frecuencia y acepta milisegundos o "blur". Hacer que phx-submit apunte al mismo evento significa que LiveView gestiona la tecla Enter en lugar de que el navegador envíe el formulario. La guía de bindings de formularios explica que los parámetros del formulario llegan como un mapa con claves de tipo string que llevan el nombre de los inputs, por ejemplo %{"q" => "pho"}.
Las plantillas interpolan con {@query}. Los tutoriales que usan <%= @query %> son anteriores a la sintaxis actual. El atributo :for es la forma abreviada de HEEx para una comprehension.
El Elixir que necesitas conocer para este archivo:
- Átomos como
:ok,:noreplyy:queryson constantes con nombre, similares a los enums de strings. - Tuplas como
{:noreply, socket}son agrupaciones de valores de tamaño fijo. Los callbacks las devuelven para indicar qué ha ocurrido. - Cabeceras de función con pattern matching: las dos cláusulas de
handle_event/3son definiciones independientes, y Elixir ejecuta aquella cuyo primer argumento coincide con el nombre del evento.%{"q" => q}desestructura los parámetros igual queconst { q } = paramsen JavaScript. - Pipes:
|>pasa el valor de su izquierda como primer argumento de la siguiente llamada. - Inmutabilidad:
assigndevuelve un socket nuevo. Nada se modifica en el sitio.
¿Cómo se añade JavaScript del lado del cliente con un hook colocado?
Un hook colocado te permite escribir el JavaScript de un componente en una etiqueta <script> justo al lado de su marcado. El nombre del hook empieza por un punto, y el elemento al que se asocia debe tener un id de DOM único. La guía de interoperabilidad con JS cubre la API completa de hooks.
El siguiente ejemplo añade atajos de teclado: / pone el foco en el campo de búsqueda y Escape lo vacía. Sustituye el <input> de render/1 por:
<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 se ejecuta cuando el elemento se añade a la página y destroyed cuando se elimina, de modo que el listener se registra una sola vez y luego se limpia. pushEvent("clear", {}) activa la misma cláusula handle_event("clear", ...) que usa el botón, así que cliente y servidor comparten una única ruta de código. Vaciar el campo y quitarle el foco antes de enviar el evento mantiene el valor visible sincronizado con el estado del servidor.
El módulo ColocatedHook extrae el script en tiempo de compilación. Un nombre que empieza por punto se asigna al espacio de nombres del módulo, y la funcionalidad requiere Phoenix 1.8 o posterior. El app.js generado por mix phx.new ya registra estos hooks:
import {hooks as colocatedHooks} from "phoenix-colocated/my_app"
// ...
hooks: {...colocatedHooks}
¿Cuándo encaja LiveView y cuándo no?
Cada evento que gestiona handle_event/3 es un viaje de ida y vuelta al servidor. Un LiveView se percibe instantáneo en una red rápida y lento en una red lenta. Los cambios puramente visuales, como abrir un desplegable, pueden ejecutarse en el navegador con comandos de Phoenix.LiveView.JS sin llegar nunca al servidor.
En localhost esa latencia queda oculta. El assets/js/app.js generado por mix phx.new ya expone el socket como window.liveSocket, así que, para simular una conexión lenta, ejecuta esto en la consola del navegador:
liveSocket.enableLatencySim(500)
// later
liveSocket.disableLatencySim()
El simulador de latencia añade los milisegundos que le indiques como latencia unidireccional hacia y desde el servidor. En producción, las reproducciones de sesión (session replays) de aplicaciones LiveView en conexiones lentas muestran el mismo problema desde el punto de vista del usuario: un input que va por detrás de lo que se escribe y un parpadeo cuando la página se desconecta y se vuelve a conectar.
LiveView tiene otros dos costes. El soporte offline es complicado, porque los eventos necesitan una conexión activa con el servidor. Además, tienes que ejecutar Elixir en producción, con un proceso por cada página conectada.
| Situación | Mejor opción | Por qué |
|---|---|---|
| Pantallas CRUD, dashboards, formularios, herramientas de administración | LiveView | Sin capa de API independiente; el estado y el renderizado permanecen en el servidor |
| Aplicaciones offline-first o con mala conectividad | React / Vue | LiveView necesita una conexión activa para gestionar eventos |
| Mucha interacción en el cliente (editores, canvas, interfaces basadas en drag and drop) | React / Vue | Cada interacción cuesta un viaje de ida y vuelta; los hooks ayudan, pero no sustituyen a un framework de cliente |
| Equipo sin interés en Elixir | React / Vue | Necesitas ejecutar y mantener Elixir en el servidor |
Para una visión más amplia del equilibrio entre interfaces controladas por el servidor y por el cliente, consulta esta comparación de htmx con Vue y React.
Conclusión
LiveView encaja bien en aplicaciones de tipo CRUD que, de otro modo, necesitarían una API JSON y un store en el cliente. Escribes callbacks en un único módulo, el servidor mantiene el estado y los hooks o los comandos JS cubren lo que tiene que ejecutarse en el navegador. Un buen siguiente paso es generar una aplicación Phoenix 1.8, reconstruir como LiveView uno de los formularios de tu SPA actual y activar el simulador de latencia para ver cómo se comporta en una conexión lenta antes de dar el paso definitivo.
Preguntas frecuentes
¿Qué ocurre cuando un LiveView pierde la conexión WebSocket?
El cliente se reconecta automáticamente con back-off exponencial y mount/3 vuelve a ejecutarse, por lo que se pierden los assigns que no se reconstruyan en mount/3. Los formularios que tienen tanto un atributo id como un binding phx-change se recuperan automáticamente: tras la reconexión, LiveView reenvía los valores actuales del formulario mediante el evento phx-change. Añade phx-auto-recover para disparar un evento de recuperación distinto, o asígnale el valor 'ignore' para desactivar la recuperación.
¿Cómo evito que el código de mount/3 se ejecute dos veces?
Envuélvelo en una comprobación connected?(socket). Phoenix.LiveView.connected?/1 devuelve false durante el renderizado HTTP estático y true una vez que el WebSocket se ha conectado. Por tanto, las suscripciones, los temporizadores y las consultas costosas dentro de un bloque if connected?(socket) solo se ejecutan en el proceso live. Mantén fuera de la comprobación los assigns por defecto que sean baratos, para que el renderizado estático siga generando un HTML completo e indexable.
¿Cómo pruebo un LiveView sin navegador?
Usa Phoenix.LiveViewTest, que se incluye con LiveView y se ejecuta con ExUnit. Llama a live(conn, path) para montar la vista y obtener un handle de la vista; después, contrólala con helpers como render_click y render_change y haz aserciones sobre el HTML que devuelven. Estas pruebas se comunican directamente con el proceso del LiveView, por lo que el JavaScript de los hooks del lado del cliente no se ejecuta en ellas.
¿Cuál es la diferencia entre un componente de función y un LiveComponent?
Un componente de función es una función sin estado que recibe assigns y devuelve HEEx, de modo que los eventos de su marcado van al LiveView padre. Un LiveComponent, definido con use Phoenix.LiveComponent, mantiene su propio estado y gestiona sus propios eventos cuando su marcado establece phx-target={@myself}. Ambos se ejecutan dentro del proceso del LiveView padre. Usa componentes de función por defecto y elige un LiveComponent solo cuando necesites que el estado y la gestión de eventos queden encapsulados dentro del componente.
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