Real Time Apps con Hotwire

Durante Diciembre DHH (creador Ruby on Rails, founder de Basecamp) estuvo twitteando mucho sobre algo que estaban desarrollando para su nuevo proyecto HEY y que iban a hacer release a la comunidad. Efectivamente el 22 de Diciembre lo liberaron y dieron a conocer el nombre Hotwire.

https://twitter.com/dhh/status/1341420143239450624

Como siempre en el mundo de la tecnología, todo vuelve y poder hacer server side rendering y mandar HTML “behind the scenes” parece ser lo nuevo.

En esta “nueva movida”, estoy muy de acuerdo con el amigo DHH, mantener todo un backend con toda una lógica, y tener que replicar ciertas cosas en un frontend de React/Angular/Vue o cualquier cosa que se les ocurra, es en muchísimo trabajo. Si bien creo que en ciertos casos está bueno y vale la pena el esfuerzo, muchas de las aplicaciones de hoy en día se podrían simplificar usando este “nuevo” approach.

Ahora si, sin más preámbulos veamos un poco de que se trata

¿Cómo funciona Hotwire?

Pueden leer mucho en la web, y me tomo el atrevimiento de hacer una traducción con mis palabras

Hotwire es un approach alternativo para construir aplicaciones web sin usar mucho Javascript, se envia HTML, en lugar de JSON a través del cable. Esto hace que las páginas carguen más rápido, mantienen el renderizado de los templates del lado del servidor y permite una experiencia de desarrollo más simple y productiva, sin sacrificar velocidad y experiencia asociada a una Single Page App.

Hotwire utiliza 3 cosas Turbo, Stimulus y Strada (al momento de escribir este post Strada aun no esta disponible).

El 80% de las interacciones se hace con Turbo, por lo tanto es el corazón de Hotwire. Si ya tenían experiencia con TurboLinks, es el mismo proyecto que cambió de nombre y está ahora generalizado para formularios. Turbo se usa para hacer stream de actualizaciones parciales de la página sobre WebSockets. Todo esto sin escribir nada de Javascript.

Cuando necesitamos hacer algo personalizado, ahi podemos usar Stimulus. Este es un framework de Javascript para el HTML que ya tenés, por lo que poniendo un poco de código en el HTML que ya tenés, se crean componentes Javascript. Esto no lo vamos a usar en esta prueba que voy a mostrar luego. Probablemente escriba sobre esto más adelante.

Por último Strada, parece que va a estar más relacionado con aplicaciones para dispositivos móviles.

Pueden leer más a fondo cómo funciona en el Handbook.

Manos a la obra

El ambiente de desarrollo

Vamos a usar lo mismo que en el post de Rails + Postgres, pero a nuestro docker-compose le vamos a agregar Redis, que lo necesitamos para configurar Action Cable.

version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
VARIANT: "2.7"
NODE_VERSION: "lts/*"
volumes:
# Update this to wherever you want VS Code to mount the folder of your project
..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
db:
image: postgres:latest
restart: unless-stopped
volumes:
postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
redis:
image: redis:latest
volumes:
postgres-data:
view raw docker-compose.yml hosted with ❤ by GitHub

Además de modificar el archivo database.yml, para conectar a Postgres, tenemos que cambiar cable.yml para que user redis (que es el nombre del servicio en el docker-compose) como url.

development:
adapter: redis
url: redis://redis:6379/1
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://redis:6379/1" } %>
channel_prefix: BeerStyles_production
view raw cable.yml hosted with ❤ by GitHub

El Proyecto

En muchos de los posts que estuve leyendo, para mostrar como funciona Hotwire, se usa un proyecto de un clone de Twitter.

En lugar de eso, a mi me gustaría hacer algo super sencillo que es una aplicación para crear estilos de cervezas.

Para arrancar creamos el proyecto, vamos a usar Postgres.

rails new BeerStyles -d postgresql

Ahora agregamos a nuestro gem file hotwire-rails

bundle add hotwire-rails

Una vez que tenemos esto vamos a configurar Hotwire, para esto tenemos una rake task que nos configura todo y nos pone el boilerplate para usarlo.

rails hotwire:install

Una vez que tenemos esto vamos a crear el scaffold para el estilo.

rails g scaffold Style name:string description:string

Ahora chequeamos que tengamos la configuración del punto anterior en cable.yml y database.yml.

No se olviden de crear la base de datos y correr las migraciones

rails db:create db:migrate

Por último probamos que nuestro proyecto funcione

rails s

Entramos entonces a http://localhost:3100/styles.

Usando Hotwire

Refactor previo

Vamos a modificar un poco lo que nos generó Rails para poder adaptarlo mejor a Hotwire.

Este es el código que nos generó Rails para app/view/styles/index.html.erb

<p id="notice"><%= notice %></p>
<h1>Styles</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @styles.each do |style| %>
<tr>
<td><%= style.name %></td>
<td><%= style.description %></td>
<td><%= link_to 'Show', style %></td>
<td><%= link_to 'Edit', edit_style_path(style) %></td>
<td><%= link_to 'Destroy', style, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Style', new_style_path %>
view raw index.html.rb hosted with ❤ by GitHub

Vamos a hacer un pequeño refactor. Dejamos de usar una tabla y migramos todo el código que muestra cada estilo a su propio template.

Creamos app/view/styles/_style.html.rb

<div class="card m-4">
<div class="card-title">
<%= style.name %>
</div>
<div class="card-body">
<%= style.description %>
</div>
<div class="card-footer bg-transparent border-success">
<%= link_to 'Edit', edit_style_path(style) %>
<%= link_to 'Destroy', style, method: :delete, data: { confirm: 'Are you sure?' } %>
</div>
</div>
view raw _style.html.erb hosted with ❤ by GitHub

Actualizamos app/view/styles/index.html.erb

<p id="notice"><%= notice %></p>
<h1>Styles</h1>
<%= render @styles %>
<%= link_to 'New Style', new_style_path %>
view raw index.html.erb hosted with ❤ by GitHub

Turbo Streams

Para que funcione Hotwire, y exista una comunicación con todos los clientes que están viendo la aplicación, vamos a usar Turbo Streams.

Lo que hacemos es colgarnos de los eventos del modelo create, update y destroy. Cuando algo de eso pasa en el modelo, tomamos alguna acción en el stream.

Vamos a modificar el modelo de Styles app/models/style.rb

class Style < ApplicationRecord
after_create_commit {broadcast_prepend_to "styles"}
after_update_commit {broadcast_replace_to "styles"}
after_destroy_commit {broadcast_remove_to "styles"}
end
view raw style.rb hosted with ❤ by GitHub

Ahora cada vez que pase algo en el modelo, Rails va a enviar un mensaje al canal styles. Por ejemplo, si se crea un nuevo estilo, se va a agregar `brodcast_prepend_to`.

Ahora tenemos que suscribirnos a ese canal, entonces vamos a modificar, app/view/styles/index.html.erb.

En este caso vamos a agregar un turbo_stream_from con el que vamos a establecer la conexión entre el WebSocket y el stream de styles. Después envolvemos la lista de estilos con turbo_frame_tag, esto nos va a permitir parchear el DOM, con lo que se publique en el stream de styles desde el backend

<p id="notice"><%= notice %></p>
<h1>Styles</h1>
<%= turbo_stream_from "styles" %>
<%= turbo_frame_tag "styles" do %>
<%= render @styles %>
<%end%>
<%= link_to 'New Style', new_style_path %>
view raw index.html.rb hosted with ❤ by GitHub

También vamos a actualizar app/view/styles/_style.html.erb envolviendo todo dentro de un nuevo turbo_frame_tag, pero esta vez agregando además el id del estilo.

Es importante usar la función dom_id(style), porque si ponemos sólo style nos va a poner como ID la dirección de memoria de nuestro estilo. Este cambio lo hacemos para poder referenciar cada estilo en el DOM y poder modificarlos o borrarlos.

<%= turbo_frame_tag dom_id(style) do %>
<div class="card m-4">
<div class="card-title">
<%= style.name %>
</div>
<div class="card-body">
<%= style.description %>
</div>
<div class="card-footer bg-transparent border-success">
<%= link_to 'Edit', edit_style_path(style) %>
<%= link_to 'Destroy', style, method: :delete, data: { confirm: 'Are you sure?' } %>
</div>
</div>
<% end %>
view raw _style.html.rb hosted with ❤ by GitHub

Ahora lo que podemos hacer es probarlo, abrimos una consola de Rails en la terminal de VS Code.

rails c

Y desde ahi podemos crear, borrar y editar estilos y ver como sin refrescar el browser se actualizan.

Edicion inline

Para hacer esto, vamos a hacer un pequeño refactor de app/views/styles/edit.html.rb.

Envolvemos nuevamente en un turbo_frame_tag el form, borramos los links que teníamos y creamos un nuevo link de cancel.

<h1>Editing Style</h1>
<%= turbo_frame_tag dom_id(@style) do %>
<%= render 'form', style: @style %>
<%= link_to 'Cancel', styles_url %>
<% end %>
view raw edit.html.rb hosted with ❤ by GitHub

También tenemos que cambiar en el app/controllers/styles_controller.rb, el lugar a donde redirige cuando hacemos una actualización de un estilo. Buscamos el método update y modificamos la redirección en caso de que el update fue exitoso. También actualizamos la rama del if en el caso de que no sea existo, agregando una línea para que nos pueda dar el estilo que se quiso crear y podamos mostrar las validaciones.

def update
respond_to do |format|
if @style.update(style_params)
format.html { redirect_to styles_url, notice: "Style was successfully updated." } # Cambió el redirect_to
format.json { render :show, status: :ok, location: @style }
else
#Agrego esta linea
format.turbo_stream { render turbo_stream: turbo_stream.replace(@style, partial: "styles/form", locals: { style: @style}) }
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @style.errors, status: :unprocessable_entity }
end
end
end
view raw styles_controller.rb hosted with ❤ by GitHub

Ahora cuando editamos un estilo se va a desplegar el formulario de actualización en la misma lista de estilos.

Crear nuevos estilos

Para poder crear estilos, vamos a agregar el formulario de creación en el archivo de app/views/styles/index.html.erb (linea 5)

<p id="notice"><%= notice %></p>
<h1>Styles</h1>
<%= turbo_stream_from "styles" %>
<%= render "styles/form", style: @style %>
<%= turbo_frame_tag "styles" do %>
<%= render @styles %>
<%end%>
<%= link_to 'New Style', new_style_path %>
view raw index.html.rb hosted with ❤ by GitHub

Tenemos que hacer algunos cambios en el controller. Hay que crear una variable @style en el método index y también hacer los mismos cambios que hicimos en el método update, ahora en el método create.

def index
@styles = Style.all
@style = Style.new
end
def create
@style = Style.new(style_params)
respond_to do |format|
if @style.save
format.html { redirect_to styles_url, notice: "Style was successfully created." }
format.json { render :show, status: :created, location: @style }
else
format.turbo_stream { render turbo_stream: turbo_stream.replace(@style, partial: "styles/form", locals: { style: @style}) }
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @style.errors, status: :unprocessable_entity }
end
end
end
view raw styles_controller.rb hosted with ❤ by GitHub

Ahora si tenemos un CRUD completo, utilizando Hotwire, de esta forma podemos hacer sitios mucho más dinámicos sin necesidad de tener un frontend hecho en Angular, React o agregando toneladas de javascript.

Si quieren ver el código del proyecto se los dejo acá:

ignaciojonas/hotwire-test

Application to Test Hotwire This app is a default Rails example to CRUD beer styles using Hotwire. There is a step by step guide in my blog (Spanish). In order to make this app easily run, you can use VSCode and Remote Containers. After reopen the folder in the container, just excute in the terminal.

Espero que puedan empezar a usar Hotwire en sus proyectos.

Nos leemos!