Phoenix LiveView en 2026: aplicaciones web reactivas sin escribir JavaScript

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

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP