LiveView funciona mejor con Svelte
(blog.sequin.io)- Tras probar tanto una SPA en React como LiveView puro de Phoenix, el equipo de Sequin concluyó que la forma más productiva es combinar LiveView con Svelte mediante LiveSvelte
- LiveView permite crear interfaces ricas sin una SPA, usando estado en el servidor y actualizaciones incrementales del DOM, pero en cuanto se necesita estado del cliente, se mezclan módulos JS, hooks y estado de LiveView, y los límites se vuelven difusos
- En LiveView puro, LiveView, LiveComponent y Component tienen ciclos de vida, paso de props y formas de comunicación distintos, lo que hace que refactorizar y ubicar el estado sea más engorroso que en React
- LiveSvelte ofrece un límite bidireccional claro, donde Elixir envía props hacia abajo y Svelte sube eventos con
live.pushEvent - Con esta combinación, el routing, la lógica de negocio y la preparación de datos pueden quedar en el servidor, mientras Svelte se encarga del estado de UI, las animaciones y el renderizado condicional, reduciendo la complejidad de una SPA
El modelo de desarrollo que ofrece LiveView
- En una app tradicional con renderizado en servidor, el servidor no mantiene estado y renderiza una página por cada solicitud del cliente
- En una SPA, el cliente se encarga de componer la página, lee y escribe datos mediante APIs de backend, y gestiona el estado del cliente con mecanismos como
useStatede React - LiveView mantiene al servidor a cargo del renderizado de la página, pero además conserva estado; procesa acciones del frontend y luego actualiza el DOM incrementalmente, como una SPA
- La complejidad de una SPA crece porque la app JS del cliente funciona, en la práctica, como un microservicio de frontend
- Aumentan las rutas y controladores del backend por requisitos específicos del frontend
- Una solicitud puede pasar por
onMount, llamadas a API, parsing de respuestas, router, plugins de autenticación, controladores, funciones de dominio y capas de preparación de respuesta
- La ventaja de LiveView es que permite crear una experiencia rica en el cliente sin un microservicio de frontend
- Se puede consultar la base de datos cerca de la función que renderiza una tabla
- Cuando llega una fila nueva, se hace push a la tabla y LiveView actualiza el cliente
- Frente al renderizado tradicional en servidor, que reconstruye todo el estado en cada solicitud, permite interacciones más parecidas a las de un framework frontend con estado
Fricciones encontradas en LiveView puro
-
El estado del lado del cliente es inevitable
- LiveView no puede evitar la latencia de ida y vuelta entre el servidor y el usuario
- Hay tareas como animaciones, tooltips, mostrar/ocultar elementos del DOM o deshabilitar campos de formulario que a veces deben manejarse en el cliente
- En un formulario con dos dropdowns dependientes, para una buena UX hay que deshabilitar el segundo dropdown apenas se selecciona el primero, y volver a habilitarlo cuando el servidor haya cargado la nueva lista
- Implementar este comportamiento solo con LiveView requiere combinar varios conceptos
- Deshabilitar el segundo dropdown al cambiar el primero usando el módulo JS de LiveView
- Registrar un listener de eventos en el segundo dropdown con un hook y enviar desde el backend una acción para volver a habilitarlo
- Si debe volver a habilitarse de forma condicional, también puede entrar en juego el estado de LiveView
- Cuando JS, hooks y estado de LiveView se usan juntos, parte del estado del DOM queda fuera de LiveView, y resulta difícil establecer un patrón consistente
-
Límites de estado distintos a los de React
- React maneja la mayoría de las transiciones con los conceptos centrales de estado y acciones, y el límite entre el estado del DOM y el estado del componente es relativamente claro
- Como no hay latencia entre las acciones del cliente y el estado del cliente, React puede tratar acciones y transiciones mediante el paradigma de estado
- Como el estado de LiveView está del lado del servidor, hay que considerar la latencia entre una acción del cliente y el estado del servidor
- En el ejemplo de un campo de entrada, React pasa por la ruta del estado antes de que se ingrese un carácter, y el componente se vuelve a renderizar con cada pulsación de tecla
- LiveView se parece más a un modelo en el que el usuario cambia primero el campo de entrada y, poco después, el estado del servidor se entera y reacciona
- Cuando la latencia es casi nula, puede verse como React, pero a medida que la latencia crece, la diferencia entre ambos modelos se vuelve clara
La complejidad que generan tres tipos de componentes
- LiveView tiene tres tipos de componentes: LiveViews, LiveComponents y Components
- LiveViews y LiveComponents se parecen a los componentes con estado de React, mientras que Components se parece a los componentes funcionales
- LiveView siempre es el componente padre de nivel superior, y debajo de él se renderizan LiveComponents y Components
- En React, es fácil moverse entre componentes con estado y componentes funcionales agregando o quitando el hook
useState- Ambos reciben props de la misma manera
- Salvo por el estado, tienen el mismo conjunto de capacidades
- También registran eventos del DOM y responden de la misma manera
- En LiveView, las diferencias entre los tres tipos de componentes son grandes, por lo que refactorizar un LiveView a LiveComponent es engorroso
- LiveViews y LiveComponents tienen sintaxis distinta para renderizado y paso de props
- Tienen ciclos de vida distintos
- Sus opciones de comunicación son distintas: en LiveViews se usa
send, y en LiveComponents,send_update - LiveComponents no son procesos, así que no pueden interactuar con otras partes del sistema como lo hacen LiveViews
Limitaciones de LiveComponent
- LiveView es un proceso de Elixir, por lo que tiene
pid, estado e inbox, y puede comportarse como otros procesos del sistema Elixir/OTP - En LiveView se puede usar pub/sub para suscribirse a cambios globales del sistema
- LiveComponent no es un proceso propio, sino un módulo que LiveView invoca
- Como el proceso LiveView padre conserva el estado de los componentes hijos, LiveView también debe encargarse del routing de mensajes hacia esos LiveComponents hijos
- Para que LiveComponents tuvieran gestión de estado y manejo de acciones independientes, cada uno tendría que ser un proceso, pero el modelo actual no funciona así
- En el desarrollo real, aunque se quiera enviar eventos o acciones a un LiveComponent, no hay una buena forma de hacerlo, y queda ambiguo si
send_updatedebe usarse para transmitir acciones o para parchear estado- Si se usa para parches de estado, también hay que pensar cómo distinguir entre montaje y actualización en la cláusula
update
- Si se usa para parches de estado, también hay que pensar cómo distinguir entre montaje y actualización en la cláusula
El límite que crea LiveSvelte
- LiveSvelte permite renderizar componentes Svelte desde LiveView
- Dentro del
renderde LiveView, se pueden codificar estructuras de datos de Elixir como un plain map para el frontend y pasarlas como props al componente Svelte - El componente Svelte recibe props con
export lety se comunica con LiveView mediante la propliveque ofrece LiveSvelte - Desde Svelte se pueden enviar eventos al servidor con
live.pushEvent- En el ejemplo, un bloque reactivo de Svelte envía al servidor el evento
"form_updated"cuando cambiaform - LiveView decodifica los parámetros del frontend en
handle_event("form_updated", ...)y actualiza el changeset - Los errores de validación del changeset vuelven al frontend mediante la prop
errors
- En el ejemplo, un bloque reactivo de Svelte envía al servidor el evento
- Los datos bajan de Elixir a Svelte como props, y desde Svelte suben a LiveView como eventos por WebSocket
- El proceso LiveView puede actualizar las props en cualquier momento para hacer que el componente Svelte se vuelva a renderizar
Propiedades clave de la combinación de LiveView y Svelte
- La clave de esta combinación es que un proceso backend con estado y un componente frontend con estado funcionan juntos
- Tres propiedades sostienen esta combinación
- El backend controla las props del componente frontend
- Tanto el frontend como el backend tienen estado
- Ambos lados tienen un canal privado de comunicación bidireccional por el que cualquiera puede iniciar mensajes
- La primera propiedad es posible gracias a la forma de renderizar de LiveView
- El resultado del rerenderizado del servidor se hace push automáticamente al cliente y se aplica
- El servidor puede actualizar las props del componente como si fuera un componente padre en JS
- La segunda propiedad proviene de que LiveView es un proceso de Elixir
- Los procesos de Elixir son una forma de encapsular estado
- La tercera propiedad es posible gracias a la conexión persistente de WebSocket que ofrece LiveView
Diferencias con una SPA
- El routing del navegador ocurre por completo en el backend, lo que simplifica la estructura
- En una SPA típica hay que mantener dos tipos de rutas: rutas de navegador y rutas de API
- El backend tiene estado y conoce la ruta actual y el recurso en el que se está trabajando
- Cada acción puede procesarse como un cambio del propio estado del backend, en vez de reconstruir el estado desde cero
- La comunicación entre frontend y backend es privada y acoplada
- No se agregan rutas públicas al servidor como llamadas RPC para soportar un único componente
- Cuando se ve
pushEventen el cliente, se sabe que el handler correspondiente está en el módulo Elixir que colabora con él
- La funcionalidad suele dividirse en dos archivos
- El módulo backend llama funciones backend, como consultas a la base de datos
- El frontend importa el componente y los estilos
- La ida y vuelta entre frontend y backend no pasa por una pila de módulos API, router y controladores
- También se reduce el procedimiento de comunicación
- El backend puede notificar cambios al frontend con solo actualizar props
- El frontend puede enviar
pushEventsin handlers para tokens vencidos, timeouts o manejo de fallas - Si el WebSocket está abierto, el servidor está funcionando; si está cerrado, LiveView muestra un banner global de “disconnected”
Separación de responsabilidades y estructura del código
- En esta estructura, es como si se eliminara el microservicio de frontend
- La lógica de negocio queda en el backend
- Cómo se cargan los datos
- Qué datos se cargan
- Cómo se ordenan y filtran
- Validadores
- El código frontend en Svelte se vuelve simple
- Bloques
if/endpara renderizado condicional - Animaciones
- Funciones
pushEventsimples que envían datos al servidor
- Bloques
- En un frontend SPA típico puede acumularse mucha lógica
map,reduceyfilterpara procesar datos del servidor, preparar datos para mostrar y preparar datos para enviar al servidor - En una app con LiveSvelte, ese procesamiento puede hacerse del lado del servidor
- LiveView prepara los datos en la forma que necesita el componente Svelte
- La complejidad permanece dentro del lenguaje del servidor, las estructuras de datos del servidor y el conjunto de pruebas del servidor
- LiveView y el componente Svelte no son tanto dos elementos débilmente acoplados, sino dos mitades de una misma funcionalidad
- LiveView solo renderiza ese componente Svelte
- Ese componente Svelte solo es renderizado por ese LiveView
Qué cambia frente a LiveView puro
- El enfoque de LiveSvelte acepta el estado y las transiciones de estado del frontend
- La capa de límite entre frontend y backend se vuelve clara
- Se puede aprovechar el paradigma maduro de componentes de Svelte
- También se puede usar el ecosistema de frameworks JavaScript, al que es difícil acceder usando solo LiveView puro
- Svelte ofrece primitives de animación
- Al mover más responsabilidades al frontend, se reduce la sensación de quedar atrapado en una zona intermedia incómoda
Por qué eligieron Svelte y dónde queda LiveView
- Eligieron LiveSvelte porque no había una biblioteca de LiveView para React con un nivel de madurez similar
- Svelte se considera un framework frontend liviano y rápido en estado básico y reactividad
- Como LiveView se encarga de una parte importante de la gestión de estado, el manejo de estado del lado de Svelte se mantiene simple
- También prefieren las capacidades de plantillas de Svelte frente a React
- Se puede usar
if/elseen lugar de operadores ternarios - Se puede usar asignación condicional de atributos
- Se puede usar
- Se espera que las runes de Svelte 5 hagan que Svelte sea más fácil de aprender y razonar
- LiveView brilla más como backend-for-frontend: renderiza componentes frontend, realiza actualizaciones incrementales, mantiene un proceso backend con estado y ofrece una API de WebSocket
1 comentarios
Opiniones de Hacker News
Un patrón que se usa a menudo en juegos multijugador es, básicamente, ejecutar el mismo código tanto en el cliente como en el servidor
El código del cliente se ejecuta primero prediciendo el estado del servidor, y cuando llega el estado del servidor, sobrescribe el estado del cliente
En los juegos, el cliente puede adivinar bastante bien el resultado de sus propias entradas, pero no conoce las entradas de otros jugadores, así que la palabra “predicción” encaja bien
Pero este enfoque también se puede usar para responder de inmediato mientras se espera el estado oficial del servidor, como al activar/desactivar un dropdown o mostrar un spinner de carga
También hay mucho estado del cliente que no hace falta ejecutar en el servidor, como sistemas de partículas o ragdolls, y si hay una conexión persistente con el servidor, no veo por qué no podría funcionar también en un paradigma reactivo
Es un clon de https://generals.io/ y está hecho con LiveSvelte
En realidad es un juego de mesa y corre a 2 ticks por segundo, así que no necesita predicción, pero sí usa actualizaciones optimistas para mostrar el estado del juego antes de que llegue la actualización del servidor
Si se desincroniza con el servidor, el servidor sobrescribe el estado del juego
Toda la lógica del juego está en Elixir, y para hacer predicción correctamente habría que compartir la lógica entre servidor y cliente
Si no, terminas escribiendo la lógica dos veces, y eso es el camino al desastre
No lo investigué, pero una posible solución sería escribir toda la lógica del juego en gleam(https://gleam.run/)
Gleam es compatible con Elixir y también puede compilar a JavaScript, así que en teoría se podría ejecutar el mismo código en servidor y cliente
Dicho eso, “pues escríbelo todo en js y ya” también es un argumento bastante válido, y probablemente estaría de acuerdo
Aun así, la ventaja que se obtiene es poder usar BEAM y sus buenas funciones para sistemas distribuidos en tiempo real, que encajan perfecto con juegos multijugador salvo por el frontend
Si alguien muere y el cuerpo sale volando o queda trabado en una pose graciosa, nadie más lo ve, y tampoco se puede volver a ver en una repetición porque cada cliente lo renderiza distinto
Si no lo capturas con una grabación en tiempo real, desaparece para siempre
En un juego que a veces es tan ridículo como Overwatch, da pena que nadie más pueda verlo contigo
https://arstechnica.com/gaming/2019/10/explaining-how-fighti... es una excelente explicación, y hay discusiones relacionadas en https://news.ycombinator.com/item?id=34399790 y https://news.ycombinator.com/item?id=26289933
Si entra en juego la persistencia del servidor, como en un MMO, también hay que considerar los cuellos de botella de entrada/salida
https://prdeving.wordpress.com/2023/09/29/mmo-architecture-s... es un artículo interesante desde esa perspectiva, y la discusión está en https://news.ycombinator.com/item?id=37702632
Por ejemplo, un formulario live/interactivo podría parecer funcionar bien al actualizar datos derivados en la página, pero sería problemático si luego todo se revierte por un error del servidor o un problema de red
Peor aún, el usuario podría cerrar la página o navegar a otra sin saberlo
Me gusta la combinación LiveView + Svelte
En ElixirConf 2022 presenté cómo combinar ambos, pero quienes contribuyen a live_svelte fueron los que lo hicieron realmente usable
Creo que las apps con una experiencia de usuario rica siempre necesitan estado del lado del cliente
Especialmente viviendo en Nueva York, la conexión de red en movimiento no es algo que se pueda dar por sentado
Una función potente que el artículo no cubre es que, usando pubsub de Phoenix, también se pueden enviar de forma reactiva a todos los clientes los cambios de estado del lado del servidor ocurridos en otros servidores
Para manejar tráfico medio o mayor, es bastante común tener varios servidores web
Si quieres una experiencia de usuario tipo LiveView pero que resista redes inestables, agregué bindings de Svelte 5 a Triplit[1]
Puedes escribir consultas del lado del cliente y sincronizarlas con el servidor mediante WebSocket
Para desarrolladores de Elixir quizá no sea tan atractivo, pero si Svelte está en el centro, Triplit es una buena opción
[1] https://www.triplit.dev/docs/frameworks/svelte
Cuando entra una fila nueva, conviene evitar simplemente empujarla a la tabla y dejar que LiveView actualice el cliente en apps de trabajo donde esas filas son interactivas.
Por la latencia cognitiva, es fácil que el usuario haga clic en el elemento equivocado, envíe un email al cliente equivocado o reembolse la transacción incorrecta.
La experiencia de usuario preferida es un banner fijo tipo “Los datos cambiaron. Haz clic aquí para actualizar”.
Si hay prisa, habría que renderizar las filas nuevas solo como agregados y asegurarse de que la posición de scroll no cambie.
En BeaconCMS se están usando Svelte y LiveView juntos.
Sin duda hay buenos casos de uso donde se necesita un control más fino de la UI en el cliente, pero hay que tener cuidado con que el equipo adopte Svelte + LiveView de forma generalizada para todo.
Incluso en Phoenix, LiveView no siempre es la respuesta; a veces basta con páginas renderizadas normales.
No hay que elegir todo o nada.
Como dice el artículo, hay buenos casos de uso para salirse del “enfoque LiveView”.
Si el round trip es de 1,000 ms, habría que considerar otras opciones, pero también hay equipos que no pueden tener servidores geográficamente cercanos por razones como el costo, así que la gestión de estado del lado del cliente puede ser una solución.
¿O sea que, en vez de gestionar estado del cliente, van a gestionar estado tanto del cliente como del servidor?
Aunque tenga la ventaja de no tener que crear otra API, no parece una mejora.
Siempre hay estado que el cliente debe rastrear y estado que el servidor debe rastrear, y a eso se superponen estados temporales que solo necesitan vivir en memoria y estados que deben persistir entre sesiones.
Las formas de gestionar esto son casi infinitas y, como en JavaScript “nada es la respuesta correcta”, uno termina usando la forma que impone el framework o parches improvisados, es decir, caos.
En la última app de una sola página compleja que escribí, hacía que el cliente sincronizara cada vez que cargaba la página.
Todo estado o recurso local tenía un hash basado en fecha, y si difería del valor del servidor, el servidor enviaba la versión actualizada.
Daba igual si era una variable simple, un objeto JSON enorme o un blob completo de imagen/audio.
Si el cliente hacía algo que requería cambiar estado del servidor, enviaba esa actualización de estado por WebSocket, y el 99% de la app estaba basada en WebSocket.
No usé ningún framework ni patrón especial; al escribir código, si pensaba “esto debería rastrearlo el servidor”, enviaba el estado nuevo al servidor como mensaje y dejaba que el servidor decidiera si sincronizarlo al cargar la página.
Para este tipo de gestión de estado, creo que este es prácticamente el mecanismo más simple posible.
WebSocket ayuda muchísimo con la gestión de estado.
Las aplicaciones reales necesitan estado tanto en el cliente como en el servidor.
Como el servidor no puede confiar en el cliente, debe tener el estado autoritativo, pero el cliente también debe poder volver a renderizar en respuesta a interacciones del usuario sin un round trip.
No hay que hacerlo con todo, solo usarlo cuando tenga sentido.
Otra cosa que me gusta es que se puede usar Svelte como lenguaje de plantillas en lugar de Heex.
Si no lo necesitas, no hay razón para hacerlo, y las razones para necesitarlo son rendimiento y experiencia de usuario.
Si una aplicación web tiene mucha interacción de usuario y necesita JavaScript del lado del cliente, y además hay muchos experimentos que hacen que el bundle cambie en cada request, vas a querer dividir la lógica entre servidor y cliente para reducir la cantidad de JavaScript que envías al cliente.
Si no, terminas con un bundle de JavaScript de varios MB que contiene todos los casos posibles que la aplicación podría encontrar.
Puede estar bien en una app como la versión web de Photoshop, donde el usuario entiende el tiempo de carga inicial, pero en apps como Stripe o Gmail, donde se espera que carguen de inmediato, quieres reducir la latencia inicial de la página.
También podrías moverlo todo al servidor, pero, como le pasó a GitHub, eso empeora la interactividad de la página porque acciones del usuario que deberían responder de inmediato necesitan un round trip al servidor.
Entonces la velocidad de la luz y la distribución de servidores se vuelven un problema.
También se puede cargar el bundle de forma diferida con cosas como async import, pero el JavaScript cargado de forma diferida luego envía otra request para traer sus propios datos, lo que genera un problema de cascada.
Cuando te topas con todos estos problemas, al final buscas una solución que facilite manejar el problema complejo de compartir lógica/estado entre cliente y servidor.
Este enfoque tiene literalmente un límite de la velocidad de la luz: “hay un límite a qué tan cerca puede estar el servidor del usuario”.
El siguiente paso es compilar el servidor a WebAssembly y enviarlo al cliente.
Así puedes renderizar una respuesta de forma optimista mientras esperas la respuesta real del servidor.
Suena un poco loco, pero lo hice en un proyecto real y se sintió como magia.
Ejecutar el servidor en el cliente no resuelve eso y, al final, la latencia de red de la persistencia sigue ahí.
El estado del servidor que debe ganar en última instancia también es esa persistencia.
Mucha sobreingeniería termina implementándose de verdad incluso sin una gran razón.
Como hice LiveSvelte, avísenme si tienen alguna pregunta.
En general, siempre quise crear apps con este tipo de modelo.
Orientado a eventos, con actualizaciones bidireccionales en tiempo real entre el servidor y el cliente, eventos ordenados, estado local/remoto, cosas así.
No conocía LiveView y nunca he usado lenguajes de la familia Erlang, pero definitivamente parece que están dando en el clavo con algo.
El modelo tradicional de solicitud-respuesta suele causar, de forma sutil, problemas de consistencia y de datos obsoletos.
Si la última década fue la de integrar conceptos de programación funcional en lenguajes mainstream, ojalá la próxima sea la de integrar la programación orientada a mensajes con estado en el full stack mainstream.
Como referencia, los tres primeros los hice yo o ayudé a crearlos.
Si sigues una guía y trasteas un poco, probablemente puedas tener una app básica de chat en Phoenix corriendo en el navegador en unas dos horas.
Es una buena solución.
En mi app uso controladores Stimulus reutilizables junto con LiveView, y eso también funciona sin fricción.
En general, desarrollar con LiveView es agradable, pero cuanto más lo uso en escenarios reales, más noto también las ventajas de frameworks HTTP sin estado como Hotwire.
Se sienten más performantes y más resistentes a las reconexiones, y reducen la necesidad de poner más servidores cerca del usuario por estabilidad.
Pero Stimulus no ofrece una forma elegante de mantener estado en el cliente.
Es más bien “el estado de una aplicación Stimulus existe como atributos del DOM”, así que no es mejor que vanilla JS o jQuery.
No he usado Stimulus en un proyecto real, así que es posible que los valores y los callbacks de cambio ofrezcan una experiencia mejor de lo que imaginé al principio.
Esta combinación tiene justo el nivel de complejidad adecuado, y Svelte, cuando se usa con LiveView, da la sensación de ocuparse de muchas cosas que ni siquiera son un problema en primer lugar.
Lo que se necesita es vanilla JS o una capa delgada encima, no un framework completo.
De todos modos, no hace falta escribir mucho JavaScript.
El JS de mi app en producción no supera las 200 líneas, y probablemente incluso podría eliminar algunos controladores Stimulus.
Así de bueno es LiveView.
También armé un adaptador de hooks Stimulus-LiveView bastante hacky para que los controladores Stimulus puedan enviar eventos directamente al proceso de LiveView.
Y LiveView, salvo que te hayas tomado demasiado el Kool-Aid al estilo fly.io, no exige necesariamente servidores distribuidos geográficamente.
También maneja muy bien las desconexiones.
No tuve que hacer nada aparte para soportar actualizaciones sin downtime.
Todo el proceso en el que el cliente pierde la conexión WebSocket, se reconecta a la nueva versión y restaura el estado viene manejado automáticamente por defecto.
¿Qué más se puede pedir?
En nuestra empresa obtuvimos grandes beneficios usando LiveView junto con Alpine, especialmente porque no podemos permitir
evalen el cliente y usamos modo CSP.Cuando vi LiveSvelte por primera vez, parecía muy nuevo y poco probado, y también tenía implicaciones no resueltas en entornos CSP estrictos.
Me alegra ver que ahora parece haberse vuelto bastante útil.
Nuestro patrón podría llamarse, más o menos, LiveAlpine.
Si un componente necesita HTML, usamos HTML Component; si tiene estado del lado del servidor, usamos Live Component.
Si necesita comportamiento o estado del lado del cliente, también definimos un componente Alpine.
Si hay que recibir eventos del cliente en el servidor o hacer solicitudes HTTP, también definimos un Phoenix Hook.