La promesa de Phoenix LiveView siempre fue tentadora: interfaces web que reaccionan a eventos en tiempo real, sin escribir una línea de JavaScript para la lógica de negocio. En 2026, con LiveView 1.0 estable desde hace tiempo, esa promesa se ha convertido en algo que muchos equipos usan en producción. Merece la pena entender cómo funciona y dónde tiene sentido.
El problema que resuelve LiveView
El modelo habitual de una SPA (React, Vue, Svelte) implica mantener dos aplicaciones: una en el servidor y otra en el cliente, sincronizadas mediante una API. Eso significa más código, más puntos de fallo, más complejidad de despliegue y el coste de coordinar dos equipos o dos contextos mentales.
LiveView plantea otra cosa: toda la lógica vive en el servidor Elixir. Cuando el usuario hace click en un botón, el evento viaja por WebSocket al servidor, el servidor actualiza su estado interno, calcula el diff del HTML y lo envía de vuelta al navegador. El cliente solo aplica ese diff al DOM. El JavaScript que hay en la página (el hook de LiveView) no contiene lógica de negocio.
Cómo funciona técnicamente
Cada LiveView es un proceso GenServer en el servidor. Cuando un usuario abre la página, se establece una conexión WebSocket y el proceso queda vivo mientras dure la sesión. El estado de la interfaz vive en ese proceso.
defmodule MiAppWeb.ContadorLive do
use Phoenix.LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, :contador, 0)}
end
def render(assigns) do
~H"""
<div>
<p>Contador: <%= @contador %></p>
<button phx-click="incrementar">+1</button>
<button phx-click="decrementar">-1</button>
</div>
"""
end
def handle_event("incrementar", _params, socket) do
{:noreply, update(socket, :contador, &(&1 + 1))}
end
def handle_event("decrementar", _params, socket) do
{:noreply, update(socket, :contador, &(&1 - 1))}
end
end
El atributo phx-click en el HTML indica que el click debe enviarse al servidor. No hay fetch, no hay setState, no hay useEffect. El servidor recibe el evento, actualiza el socket y Phoenix calcula qué cambió en el HTML para enviarlo como diff.
HEEx: plantillas con seguridad de tipos
La sintaxis ~H""" es HEEx (HTML + Embedded Elixir). A diferencia de EEx plano, HEEx entiende la estructura HTML: detecta atributos mal cerrados en compilación, previene XSS escapando automáticamente las interpolaciones, y hace que el diff sea más eficiente porque opera a nivel de nodo DOM en lugar de texto.
Componentes en LiveView
LiveView 1.0 maduró el sistema de componentes funcionales y LiveComponents. Los componentes funcionales son funciones simples que reciben assigns y devuelven HTML:
defmodule MiAppWeb.Componentes do
use Phoenix.Component
attr :texto, :string, required: true
attr :tipo, :atom, default: :info
def alerta(assigns) do
~H"""
<div class={["alerta", alerta_clase(@tipo)]}>
<%= @texto %>
</div>
"""
end
defp alerta_clase(:info), do: "alerta-info"
defp alerta_clase(:error), do: "alerta-error"
defp alerta_clase(_), do: "alerta-default"
end
Los attr son declarativos y con tipos. El compilador avisa si usas un componente sin pasar un atributo requerido o con un tipo incorrecto. Esto, combinado con los tipos graduales de Elixir 1.18, convierte el sistema de plantillas en algo bastante robusto.
LiveComponents: estado local en el servidor
Para partes de la interfaz que necesitan estado propio sin contaminar el LiveView padre, existen los LiveComponents:
defmodule MiAppWeb.FormularioLive do
use Phoenix.LiveComponent
def mount(socket) do
{:ok, assign(socket, :errores, [])}
end
def render(assigns) do
~H"""
<form phx-submit="guardar" phx-target={@myself}>
<input type="text" name="nombre" />
<button type="submit">Guardar</button>
</form>
"""
end
def handle_event("guardar", %{"nombre" => nombre}, socket) do
# phx-target={@myself} dirige el evento a este componente, no al LiveView padre
{:noreply, assign(socket, :nombre_guardado, nombre)}
end
end
El phx-target={@myself} es clave: indica que el evento lo maneja el propio componente, no su LiveView padre. Permite encapsular lógica de formularios, tablas con paginación o cualquier widget complejo.
Streams: listas eficientes en LiveView
Una de las incorporaciones más útiles de las versiones recientes es el sistema de streams, para manejar listas largas sin guardar toda la colección en el estado del servidor:
def mount(_params, _session, socket) do
socket = stream(socket, :mensajes, Mensajes.listar_recientes())
{:ok, socket}
end
def handle_info({:nuevo_mensaje, mensaje}, socket) do
{:noreply, stream_insert(socket, :mensajes, mensaje, at: 0)}
end
Con streams, Phoenix sabe qué elementos son nuevos o han cambiado en una lista y solo envía esos diffs, sin serializar toda la colección en cada actualización.
Cuándo tiene sentido y cuándo no
LiveView brilla en aplicaciones donde el estado cambia frecuentemente y hay interacción del usuario: dashboards, formularios complejos, chats, herramientas de administración, editores colaborativos. La latencia del WebSocket es imperceptible para la mayoría de casos de uso.
Donde LiveView no es la mejor opción: aplicaciones con mucha lógica offline (una app que funciona sin conexión), casos donde el cliente necesita manipular el DOM directamente de forma intensiva (un editor de imágenes), o cuando el equipo ya tiene una SPA robusta y no hay razón para reescribirla.
Para casos donde sí necesitas algo de JavaScript del lado del cliente, LiveView tiene un sistema de hooks que permite escribir código JS que se ejecuta cuando un elemento aparece o desaparece del DOM, sin romper el modelo general.
Si te interesa comparar cómo se construyen interfaces reactivas en el ecosistema Python, el artículo sobre Python y LLMs en 2026 muestra un enfoque diferente. Y para entender la base de concurrencia que hace posible que cada usuario de LiveView tenga su propio proceso en el servidor, el artículo sobre OTP y GenServer de esta misma serie es el punto de partida.
Imagen: Pexels / Markus Winkler
