LiveView combina melhor com Svelte
(blog.sequin.io)- A equipe da Sequin concluiu, após usar tanto React SPA quanto Phoenix LiveView puro, que a abordagem mais produtiva é combinar LiveView com Svelte via LiveSvelte
- O LiveView permite criar UIs ricas sem SPA, com estado no servidor e atualizações incrementais do DOM, mas no momento em que é preciso estado no cliente, módulos JS, hooks e estado do LiveView acabam se misturando e a fronteira fica difusa
- Em LiveView puro, LiveView, LiveComponent e Component têm ciclos de vida, passagem de props e formas de comunicação diferentes, o que torna refatoração e alocação de estado mais trabalhosas do que no React
- O LiveSvelte fornece uma fronteira bidirecional clara: Elixir envia props para baixo e Svelte envia eventos para cima com
live.pushEvent - Nessa combinação, roteamento, lógica de negócio e preparação de dados ficam no servidor, enquanto estado de UI, animações e renderização condicional ficam com o Svelte, reduzindo a complexidade de uma SPA
O modelo de desenvolvimento oferecido pelo LiveView
- Aplicações tradicionais com renderização no servidor não mantêm estado, e o servidor renderiza a página a cada requisição do cliente
- Em uma SPA, o cliente monta a página, lê e grava dados via API do backend e gerencia o estado no cliente com abordagens como
useStatedo React - O LiveView continua deixando a renderização da página a cargo do servidor, mas também mantém estado e, após processar ações do frontend, atualiza o DOM incrementalmente como uma SPA
- A complexidade da SPA aumenta porque o app JS do cliente passa a funcionar como um microserviço de frontend
- Requisitos exclusivos do frontend fazem crescer o número de rotas e controllers no backend
- Uma única requisição pode passar por
onMount, chamada de API, parsing da resposta, roteador, plugin de autenticação, controller, função de domínio e camada de preparação da resposta
- A vantagem do LiveView é permitir uma experiência rica no cliente sem precisar desse microserviço de frontend
- É possível consultar o banco de dados perto da função que renderiza a tabela
- Quando uma nova linha entra, ela pode ser enviada para a tabela, e o LiveView atualiza o cliente
- Em vez de reconstruir todo o estado a cada requisição como no modelo tradicional de renderização no servidor, dá para criar interações mais próximas de um framework frontend com estado
Atritos encontrados com LiveView puro
-
Estado no lado do cliente é inevitável
- O LiveView não consegue evitar a latência de ida e volta entre servidor e usuário
- Tarefas como animações, tooltips, mostrar/esconder elementos do DOM e desabilitar campos de formulário às vezes precisam ser tratadas no cliente
- Em um formulário com dois dropdowns dependentes, a melhor UX exige desabilitar imediatamente o segundo dropdown após a escolha no primeiro e reabilitá-lo só depois que o servidor preencher a nova lista
- Para implementar esse comportamento só com LiveView, vários conceitos acabam sendo necessários ao mesmo tempo
- Quando JS, hooks e estado do LiveView são usados juntos, parte do estado do DOM passa a existir fora do LiveView, e fica difícil estabelecer um padrão consistente
-
Fronteiras de estado diferentes das do React
- O React lida com a maior parte das transições usando os conceitos centrais de estado e ações, e a fronteira entre estado do DOM e estado do componente é relativamente clara
- Como não há latência entre ação do cliente e estado do cliente, o React consegue tratar ações e transições dentro de um paradigma de estado
- No LiveView, o estado fica no servidor, então é preciso considerar a latência entre a ação no cliente e o estado no servidor
- No exemplo de um campo de entrada, no React o caractere passa pela trilha de estado antes de aparecer, e o componente é renderizado novamente a cada tecla pressionada
- O LiveView está mais próximo de um modelo em que o usuário altera primeiro o campo, e só pouco depois o estado no servidor fica sabendo disso e reage
- Quando a latência é quase zero, ele se parece com React, mas conforme a latência aumenta, a diferença entre os dois modelos fica evidente
A complexidade criada por três tipos de componente
- O LiveView tem três tipos de componente: LiveViews, LiveComponents e Components
- LiveViews e LiveComponents se parecem mais com componentes com estado do React, enquanto Components se parecem mais com componentes funcionais
- O LiveView é sempre o componente pai no topo, e abaixo dele são renderizados LiveComponents e Components
- No React, é fácil alternar entre componente funcional e componente com estado adicionando ou removendo um hook como
useState- Ambos recebem props da mesma forma
- Tirando o estado, o conjunto de recursos é o mesmo
- O registro de eventos do DOM e a forma de responder também são iguais
- No LiveView, as diferenças entre os três tipos de componente são grandes, então refatorar um LiveView para LiveComponent é trabalhoso
- LiveViews e LiveComponents usam sintaxes diferentes para renderização e passagem de props
- Os ciclos de vida são diferentes
- As opções de comunicação são diferentes: LiveViews usam
send, enquanto LiveComponents usamsend_update - LiveComponents não são processos, então não conseguem interagir com outras partes do sistema como LiveViews
As limitações do LiveComponent
- Um LiveView é um processo Elixir, então tem
pid, estado e inbox, podendo agir como outros processos do sistema Elixir/OTP - Em um LiveView, é possível usar pub/sub para assinar mudanças em todo o sistema
- Um LiveComponent não é um processo próprio, mas um módulo chamado pelo LiveView
- Como o processo LiveView pai guarda o estado dos componentes filhos, o próprio LiveView precisa cuidar também do roteamento de mensagens dos LiveComponents filhos
- Para que LiveComponents tivessem capacidade independente de gerenciar estado e processar ações, cada um precisaria ser um processo, mas o modelo atual não funciona assim
- No desenvolvimento real, mesmo quando se quer enviar eventos ou ações para um LiveComponent, não existe uma forma realmente boa de fazer isso, e fica ambíguo se
send_updatedeve servir para enviar ações ou aplicar patches de estado- Se for usado para patch de estado, também é preciso pensar em como distinguir mount de update dentro da cláusula
update
- Se for usado para patch de estado, também é preciso pensar em como distinguir mount de update dentro da cláusula
A fronteira criada pelo LiveSvelte
- LiveSvelte permite renderizar componentes Svelte dentro do LiveView
- Dentro do
renderdo LiveView, é possível codificar estruturas de dados em Elixir como plain maps para o frontend e passá-las como props para o componente Svelte - O componente Svelte recebe props com
export lete se comunica com o LiveView por meio da proplivefornecida pelo LiveSvelte - No lado do Svelte, é possível enviar eventos ao servidor com
live.pushEvent- No exemplo, um bloco reativo do Svelte envia o evento
"form_updated"ao servidor quandoformmuda - O LiveView decodifica os parâmetros do frontend em
handle_event("form_updated", ...)e atualiza o changeset - Erros de validação do changeset voltam para o frontend pela prop
errors
- No exemplo, um bloco reativo do Svelte envia o evento
- Os dados descem do Elixir para o Svelte via props, e sobem do Svelte para o LiveView via eventos em websocket
- O processo LiveView pode atualizar props a qualquer momento para fazer o componente Svelte ser renderizado novamente
Propriedades centrais da combinação LiveView + Svelte
- O ponto central dessa combinação é que um processo de backend com estado e um componente de frontend com estado funcionam juntos
- Três propriedades sustentam essa combinação
- O backend controla as props do componente de frontend
- Frontend e backend têm estado
- Os dois lados têm um canal privado de comunicação bidirecional, no qual ambos podem iniciar mensagens
- A primeira propriedade é possível graças ao modo como o LiveView renderiza
- O resultado da rerenderização no servidor é automaticamente enviado ao cliente e aplicado
- O servidor pode atualizar as props do componente como se fosse um componente pai em JS
- A segunda propriedade vem do fato de o LiveView ser um processo Elixir
- Processos Elixir são uma forma de encapsular estado
- A terceira propriedade existe por causa da conexão persistente por websocket fornecida pelo LiveView
Diferenças em relação a uma SPA
- Todo o roteamento no navegador passa a ser feito no backend, o que simplifica a estrutura
- Em uma SPA comum, é preciso manter dois tipos de rota: rotas do navegador e rotas de API
- O backend tem estado e sabe qual é a rota atual e qual recurso está sendo manipulado
- Cada ação pode ser tratada como uma mudança no estado do próprio backend, em vez de reconstruir todo o estado do zero
- A comunicação entre frontend e backend é privada e acoplada
- Não é preciso aumentar o número de rotas públicas do servidor com chamadas RPC para sustentar um único componente
- Ao ver um
pushEventno cliente, já se sabe que o handler correspondente está no módulo Elixir com o qual ele colabora
- A funcionalidade costuma ficar dividida principalmente em dois arquivos
- O módulo de backend chama funções de backend, como consultas ao banco de dados
- O frontend importa componentes e estilos
- O vai e vem entre frontend e backend não passa por módulo de API, roteador e pilha de controllers
- O fluxo de comunicação também diminui
- O backend pode sinalizar mudanças no frontend apenas atualizando props
- O frontend pode enviar
pushEventsem handlers para token expirado, timeout ou falha - Se o websocket estiver aberto, o servidor está funcionando; se estiver fechado, o LiveView mostra um banner global de “disconnected”
Separação de responsabilidades e estrutura de código
- Nessa estrutura, é como se o microserviço de frontend tivesse sido removido
- A lógica de negócio fica no backend
- Como carregar dados
- Quais dados carregar
- Como ordenar e filtrar
- Validadores
- O código de frontend em Svelte fica mais simples
- Blocos
if/endpara renderização condicional - Animações
- Funções simples de
pushEventpara enviar dados ao servidor
- Blocos
- Em um frontend SPA típico, é comum surgir muito
map,reduceefilterpara processar dados do servidor, preparar dados para exibição e preparar dados para envio ao servidor - Em um app com LiveSvelte, esse tratamento pode ficar no lado do servidor
- O LiveView prepara os dados no formato de que o componente Svelte precisa
- A complexidade permanece na linguagem do servidor, nas estruturas de dados do servidor e na suíte de testes do servidor
- LiveView e componente Svelte se parecem menos com dois elementos fracamente acoplados e mais com duas metades de uma mesma funcionalidade
- O LiveView renderiza apenas aquele componente Svelte
- E aquele componente Svelte só é renderizado por aquele LiveView
O que muda em relação ao LiveView puro
- A abordagem com LiveSvelte aceita o estado do frontend e as transições de estado
- A camada de fronteira entre frontend e backend fica mais clara
- É possível aproveitar o paradigma maduro de componentes do Svelte
- Também dá para usar um ecossistema de framework JavaScript que seria difícil de alcançar só com LiveView puro
- O Svelte oferece primitives de animação
- Ao mover mais responsabilidade para o frontend, diminui a sensação de ficar preso numa zona intermediária estranha
Por que escolher Svelte e onde o LiveView brilha
- A escolha do LiveSvelte veio do fato de não existir uma biblioteca de LiveView para React com nível semelhante de maturidade
- O Svelte é visto como um framework de frontend leve e rápido, com bom suporte nativo a estado e reatividade
- Como o LiveView assume grande parte do gerenciamento de estado, o estado do lado do Svelte continua simples
- Os recursos de template do Svelte também são preferidos em relação ao React
- É possível usar
if/elseem vez de operador ternário - Dá para usar configuração condicional de atributos
- É possível usar
- Espera-se que as runes do Svelte 5 tornem o Svelte mais fácil de aprender e de raciocinar
- O LiveView brilha mais como um backend-for-frontend que renderiza componentes de frontend, faz atualizações incrementais, mantém um processo de backend com estado e fornece uma API via websocket
1 comentários
Opiniões no Hacker News
Um padrão usado com frequência em jogos multiplayer é, basicamente, executar o mesmo código tanto no cliente quanto no servidor
O código do cliente prevê o estado do servidor e roda primeiro; quando o estado do servidor chega, ele sobrescreve o estado do cliente
Em jogos, o cliente consegue estimar razoavelmente bem o resultado das próprias entradas, mas não sabe as entradas dos outros jogadores, então o termo “previsão” se encaixa bem
Mas esse método também pode ser usado para responder imediatamente enquanto se espera pelo estado oficial do servidor, como ao ativar/desativar um dropdown ou mostrar um spinner de carregamento
Também há muito estado do cliente que nem precisa rodar no servidor, como sistemas de partículas ou ragdolls; se há uma conexão contínua com o servidor, não parece haver motivo para isso não funcionar também em um paradigma reativo
É um clone de https://generals.io/ e foi feito com LiveSvelte
Na prática, é um jogo de tabuleiro e roda a 2 ticks por segundo, então não precisa de previsão, mas usa atualizações otimistas para mostrar o estado do jogo antes da atualização do servidor
Se sair de sincronia com o servidor, o servidor sobrescreve o estado do jogo
Toda a lógica do jogo está em Elixir e, para fazer previsão corretamente, o servidor e o cliente precisariam compartilhar a lógica
Caso contrário, você acaba escrevendo a lógica duas vezes, e isso é o caminho para o desastre
Não pesquisei, mas uma solução possível seria escrever toda a lógica do jogo em gleam(https://gleam.run/)
O Gleam é compatível com Elixir e também pode ser compilado para JavaScript, então, em teoria, o mesmo código poderia rodar no servidor e no cliente
Dito isso, “é só escrever tudo em js” também é um argumento bem válido, e eu provavelmente concordaria
Ainda assim, o benefício obtido seria poder usar a BEAM e seus bons recursos para sistemas distribuídos em tempo real, o que combina perfeitamente com jogos multiplayer, tirando a parte de frontend
Quando alguém morre e o corpo sai voando ou fica preso numa pose engraçada, ninguém mais vê aquilo, e nem no replay dá para rever, porque cada cliente renderiza de um jeito diferente
Se não for capturado por uma gravação em tempo real, desaparece para sempre
Em um jogo ocasionalmente ridículo como Overwatch, é uma pena que ninguém possa ver junto
https://arstechnica.com/gaming/2019/10/explaining-how-fighti... é uma ótima explicação, e discussões relacionadas estão em https://news.ycombinator.com/item?id=34399790 e https://news.ycombinator.com/item?id=26289933
Quando entra persistência de servidor, como em MMOs, também é preciso considerar gargalos de entrada/saída
https://prdeving.wordpress.com/2023/09/29/mmo-architecture-s... é um texto interessante por esse ponto de vista, e a discussão está em https://news.ycombinator.com/item?id=37702632
Por exemplo, um formulário live/interativo pode parecer estar funcionando bem ao atualizar dados derivados da página, mas seria problemático se depois tudo fosse revertido por um erro no servidor ou um problema de rede
Pior ainda, o usuário poderia fechar a página ou navegar para outro lugar sem saber disso
Gosto da combinação LiveView + Svelte
Apresentei uma forma de combinar os dois na ElixirConf 2022, mas foram os contribuidores do live_svelte que a tornaram realmente utilizável
Acho que apps com uma experiência de usuário rica sempre precisam de estado no lado do cliente
Especialmente morando em Nova York, uma conexão de rede em movimento não é algo garantido
Um recurso poderoso que o texto não abordou é que, usando o pubsub do Phoenix, também é possível enviar de forma reativa a todos os clientes mudanças de estado do lado do servidor ocorridas em outros servidores
Para lidar com tráfego de médio porte para cima, é bastante comum ter vários servidores web
Se você quer uma experiência de usuário como a do LiveView, mas também quer resistir a redes instáveis, adicionei bindings do Svelte 5 ao Triplit[1]
É possível escrever queries no lado do cliente e sincronizar com o servidor via WebSocket
Talvez não seja tão atraente para desenvolvedores Elixir, mas, se o centro for Svelte, o Triplit é uma boa opção
[1] https://www.triplit.dev/docs/frameworks/svelte
Quando uma nova linha chega, simplesmente empurrá-la para a tabela e deixar o LiveView atualizar o cliente é algo que é melhor evitar em aplicativos corporativos nos quais essas linhas são interativas.
Por causa da latência cognitiva, fica fácil o usuário clicar no item errado, enviar um e-mail para o cliente errado ou reembolsar a transação errada.
A experiência de usuário preferida é um banner fixo do tipo “Os dados foram alterados. Clique aqui para atualizar”.
Se for urgente, as novas linhas devem ser renderizadas apenas como acréscimos, sem alterar a posição de rolagem.
No BeaconCMS, estamos usando Svelte e LiveView juntos.
Há claramente bons casos de uso em que é preciso controlar a UI no cliente com mais granularidade, mas é preciso ter cuidado para a equipe não adotar Svelte + LiveView de forma ampla para tudo.
Mesmo no Phoenix, LiveView nem sempre é a resposta; às vezes, uma página renderizada comum é suficiente.
Não se deve escolher tudo como “tudo ou nada”.
Como o texto diz, há bons casos de uso para sair do “jeito LiveView”.
Se o tempo de ida e volta for de 1.000 ms, talvez seja preciso considerar outras opções, mas há equipes que, por motivos como custo, não conseguem manter servidores geograficamente próximos; nesse caso, o gerenciamento de estado no cliente pode ser a solução.
A ideia é gerenciar o estado tanto no cliente quanto no servidor, em vez de gerenciar o estado do cliente?
Mesmo com a vantagem de não precisar criar outra API, isso não parece uma melhoria.
Sempre há estados que o cliente precisa rastrear e estados que o servidor precisa rastrear, e isso se sobrepõe a estados temporários que só precisam ficar em memória e estados que precisam persistir entre sessões.
Há praticamente infinitas formas de gerenciar isso e, como em JavaScript “nada é definitivamente a resposta certa”, acaba-se usando o jeito definido pelo framework ou gambiarras — isto é, caos.
No último aplicativo complexo de página única que escrevi, fiz com que o cliente sincronizasse sempre que carregava a página.
Todo estado local ou asset tinha um hash baseado em data; se fosse diferente do valor no servidor, o servidor enviava a versão atualizada.
Isso valia tanto para uma variável simples quanto para um objeto JSON enorme ou um blob inteiro de imagem/áudio.
Quando o cliente fazia algo que exigia mudança no estado do servidor, enviava essa atualização de estado por WebSocket, e 99% do app era baseado em WebSocket.
Não usei nenhum framework ou padrão especial; enquanto escrevia o código, se eu pensasse “isso precisa ser rastreado pelo servidor”, enviava o novo estado ao servidor como mensagem, e deixava o servidor decidir se sincronizaria no carregamento da página.
Para esse tipo de gerenciamento de estado, considero que esse é quase o mecanismo mais simples possível.
WebSocket ajuda muito no gerenciamento de estado.
Aplicações reais precisam de estado tanto no cliente quanto no servidor.
O servidor não pode confiar no cliente, então precisa manter o estado autoritativo, mas o cliente também precisa conseguir reagir às interações do usuário e renderizar novamente sem uma ida e volta.
Não é para fazer tudo assim; é para usar apenas quando fizer sentido.
Outro ponto de que gosto é poder usar Svelte como linguagem de templates em vez de Heex.
Se não for necessário, não há motivo para fazer; e os motivos para se tornar necessário são desempenho e experiência do usuário.
Se uma aplicação web tem muita interação do usuário e precisa de JavaScript no cliente, e se há muitos experimentos que fazem o bundle variar a cada requisição, dá vontade de dividir a lógica entre servidor e cliente para reduzir a quantidade de JavaScript enviada ao cliente.
Caso contrário, a aplicação acaba com um bundle JavaScript de vários MB contendo todos os cenários possíveis que ela pode encontrar.
Em um app como a versão web do Photoshop, em que o usuário entende o tempo de carregamento inicial, isso pode ser aceitável; mas em apps como Stripe ou Gmail, que se espera que carreguem imediatamente, você quer reduzir o atraso inicial da página.
Também dá para mover tudo para o servidor, mas, como o GitHub vivenciou, ações do usuário que precisam responder imediatamente passam a exigir uma ida e volta ao servidor, prejudicando a interatividade da página.
Aí a velocidade da luz e a distribuição de servidores viram problemas.
Também é possível carregar bundles sob demanda com algo como async import, mas o JavaScript carregado sob demanda faz outra requisição para buscar seus próprios dados, criando um problema de cascata.
Quando você encontra todos esses problemas, acaba procurando uma solução que torne fácil lidar com a questão complexa de compartilhar lógica/estado entre cliente e servidor.
Essa abordagem tem, literalmente, um limite imposto pela velocidade da luz: “há um limite para o quão perto o servidor pode estar do usuário”.
O próximo passo é compilar o servidor para WebAssembly e enviá-lo ao cliente.
Assim, é possível renderizar uma resposta de forma otimista enquanto se espera a resposta real do servidor.
Pode soar meio insano, mas fizemos isso em um projeto real e parecia mágica.
Executar o servidor no cliente não resolve isso e, no fim, a latência de rede da persistência continua existindo.
O estado do servidor que precisa prevalecer no fim também é essa persistência.
Muita superengenharia acaba sendo implementada mesmo sem um bom motivo.
Como criei o LiveSvelte, é só avisar se tiverem alguma dúvida.
No geral, eu sempre quis criar apps com esse tipo de modelo
Algo orientado a eventos, com atualizações bidirecionais em tempo real entre servidor e cliente, eventos ordenados e estado local/remoto
Eu não conhecia LiveView e nunca usei nenhuma linguagem da família Erlang, mas certamente parece que eles capturaram algo importante
O modelo tradicional de requisição-resposta causa, de forma sutil e frequente, problemas de consistência e dados obsoletos
Se a última década foi a de integrar conceitos de programação funcional às linguagens mainstream, espero que a próxima seja voltada a integrar programação orientada a mensagens com estado ao full-stack mainstream
A propósito, os três primeiros eu criei ou ajudei a criar
Se seguir o guia e mexer um pouco, provavelmente conseguirá colocar no ar um app de chat Phoenix básico rodando no navegador em cerca de duas horas
É uma boa solução
No meu app, uso controladores Stimulus reutilizáveis junto com LiveView, e isso também funciona de forma fluida
No geral, desenvolver com LiveView é prazeroso, mas quanto mais uso em cenários reais, mais percebo também as vantagens de frameworks HTTP sem estado como o Hotwire
Eles parecem ter melhor desempenho e ser mais resistentes a reconexões, além de reduzir a necessidade de colocar mais servidores próximos dos usuários para garantir estabilidade
Mas o Stimulus não oferece uma forma elegante de manter estado no cliente
É algo como “o estado de uma aplicação Stimulus existe nos atributos do DOM”, o que não é melhor que vanilla JS ou jQuery
Eu nunca usei Stimulus em um projeto real, então é possível que valores e callbacks de alteração ofereçam uma experiência melhor do que pensei inicialmente
Essa combinação tem exatamente o nível certo de complexidade, enquanto Svelte com LiveView parece lidar com muitas coisas que, para começo de conversa, nem são problemas
O necessário é vanilla JS ou uma camada fina por cima dele, não um framework inteiro
De qualquer forma, nem é preciso usar muito JavaScript
O JS do app em produção não passa de 200 linhas, e acho que alguns controladores Stimulus poderiam até ser eliminados
LiveView é bom assim
Também criei de forma bem hacky um adaptador de hooks Stimulus-LiveView, para que controladores Stimulus possam enviar eventos diretamente ao processo LiveView
E, a menos que o LiveView tenha bebido demais o Kool-Aid ao estilo fly.io, ele não exige necessariamente servidores distribuídos geograficamente
Ele também lida muito bem com quedas de conexão
Não precisei fazer nada específico para oferecer suporte a atualizações sem downtime
Todo o processo em que o cliente perde a conexão WebSocket, reconecta na nova versão e restaura o estado é tratado automaticamente por padrão
O que mais se poderia querer?
Nossa empresa obteve grandes benefícios usando LiveView junto com Alpine e, em especial, usamos o modo CSP porque não podemos permitir
evalno clienteQuando vi o LiveSvelte pela primeira vez, ele parecia muito novo e pouco validado, e também havia impactos não resolvidos em ambientes com CSP rigorosa
Fico feliz em ver que agora parece ter se tornado bastante útil
Nosso padrão poderia ser chamado, grosso modo, de LiveAlpine
Se um componente precisa de HTML, usamos HTML Component; se há estado no lado do servidor, usamos Live Component
Se precisamos de comportamento ou estado no lado do cliente, também definimos um componente Alpine
Se precisamos receber eventos do cliente no servidor ou fazer requisições HTTP, também definimos um Phoenix Hook