Implementando a feature defer do Remix em Go
Tags: go, webdev, javascript, remix
Published: 2024-05-27 10:01:48

Há dois meses e meio comecei a mexer com essas coisas de Go. Queria aprender o básico de Go fazendo algo que eu já sei fazer com as duas mãos amarradas pra trás, que é criar um site simples, só pra entender como funciona construir do zero um sistema nessa nova linguagem que eu queria conhecer.

O código contido nesse post está disponível em https://github.com/gabrielvincent/cat-facts

Antes de começar, importante avisar: nesse projeto eu uso [Templ](https://templ.guide/), uma biblioteca de componentes que achei muito útil. A primeira iteração desse projeto foi feita usando os HTML templates da standard lib do Go, mas é boilerplate demais pra o que eu queria mostrar aqui. Só pra dar um contexto, a cara do Templ é essa:

```go
package main

templ Title(text string) {
  <h1>{ text }</h1>
}

templ Header(props HeaderProps) {
  <header class="main-header">
    @Title(props.title)    
  </header>
}
```

Uma outra coisa que surgiu durante a implementação daquela primeira iteração foi o que eu quero mostrar aqui. Eu sou um _fanzoca_ de [Remix](https://remix.run/) e uma das coisas que senti falta no meu site de Cat Facts foi a possibilidade de [`defer`](https://remix.run/docs/en/main/guides/streaming#3-deferring-data-in-loaders) uma parte do conteúdo da página.

Uma breve contextualização pra que você não fique perdidinho ou perdidinha no pagode: Remix é um framework JavaScript para a criação de aplicações web primeiramente SSR (server-side rendered). Ou seja o HTML é gerado no servidor e enviado pro navegador. Isso significa que, pra que você possa enviar o HTML, todos os dados que estiverem contidos nele precisam já terem sido carregados no servidor. É o caso aqui do nosso site de Cat Facts. Toda vez que alguém vai até a página inicial do site, acontece o seguinte:

1. Navegador envia uma requisição ao servidor.
2. Servidor aceita a requisição.
3. Servidor faz uma requisição pra um outro servidor, o da [API de fatos sobre gatos](https://catfact.ninja/)
4. Servidor espera pela resposta da API.
5. Servidor recebe a resposta e, com ela, monta o HTML da página.
6. Servidor responde, enviando o HTML que contém excelentes fatos sobre gatos.

Ao longo desse processo todo, o usuário que surfou (eu gosto do termo "surfar na web" e é este que pretendo usar) até a página inicial viu apenas uma página em branco. Como não existe ainda um HTML pra exibir (ele tá lá no servidor, esperando a resposta da API de fatos sobre gatos, lembra?), tudo o que a pessoa pode fazer é aguardar e encarar o vazio, sem saber se o site algum dia vai carregar.

Aqui o Ryan Florence faz uma breve e excelente demonstração da funcionalidade `defer` do Remix:

{% embed https://www.youtube.com/watch?v=IKPVSV34slA %}

`defer` é uma funcionalidade específica do Remix. Não é parte do [HTTP (protocolo usado para troca de mensgens na Web)](https://datatracker.ietf.org/doc/html/rfc9112) e também não é uma funcionalidade do JavaScript. Então tive que pensar um pouco em como eu poderia replicar essa experiência no meu app Go. E foi isso aqui o que eu fiz:

## A estrutura do projeto

Esse é um projeto bem simples. Comecei com apenas uma página, servida a partir da rota `/`. A estrutura é essa:

```
.
├── main.go
├── components
│   └── components.templ
└── public
    └── css
        └── tailwind.css
```

Em `main.go`, fiz uma implementação básica do site:

```go
package main

import (
	components "catfacts/components"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"time"
)

type CatFact struct {
	Fact   string `json:"fact"`
	Length int    `json:"length"`
}

type CatFactsApiResponse struct {
	Data []CatFact `json:"data"`
}

func getFacts() ([]CatFact, error) {
	// Simula uma chamada lentona à API
	time.Sleep(2 * time.Second)

	apiUrl := "https://catfact.ninja/facts?limit=10"
	resp, err := http.Get(apiUrl)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var facts CatFactsApiResponse
	err = json.Unmarshal(body, &facts)
	if err != nil {
		return nil, err
	}

	return facts.Data, nil
}

func factsHandler(w http.ResponseWriter, r *http.Request) {

	ctx := context.Background()

	catFacts, err := getFacts()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var facts []string
	for _, catFact := range catFacts {
		facts = append(facts, catFact.Fact)
	}
	components.Index(facts).Render(ctx, w)
}

func main() {
	http.HandleFunc("/", factsHandler)
	http.HandleFunc(
		"/tailwind.css",
		func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "text/css")
			http.ServeFile(w, r, "public/css/tailwind.css")
		},
	)
	http.ListenAndServe(":3000", nil)
}
```

Aqui eu defini o que é um `CatFact` e o que é que eu espero que a API responda, abstraído no type `CatFactsApiResponse`. Defini também a função `getFacts()`, que é a função que faz a chamada HTTP e busca os fatos sobre gatos da API. Repara nas primeiras linhas dessa função:

```go
// Simula uma chamada lentona à API.
time.Sleep(2 * time.Second)
```

Isso faz com que a resposta do nosso servidor sempre leve no mínimo 2 segundos pra ser enviada. Isso é só pra simular uma operação demorada.

`factsHandler` é a função responsável por chamar `getFacts` e, uma vez que os dados tenham sido retornados pela API, renderiza o componente `components.Index`, escrevendo o HTML gerado no objeto `w`, que é o response writer. Esse é o objeto onde a resposta do servidor é escrita.

Por fim, a função `main` define as rotas e inicia o servidor. Aqui temos apenas `/`, que é página raiz do site, onde vamos exibir os fatos sobre gatos, e `/tailwind.css`, pra que fique bem fácil fazer com que o site fique tão bonito quanto qualquer gato que existe no universo.

O componente `Index`, que exibe os fatos sobre gatos, está no arquivo `components.templ`:

```go
package components

import "strconv"

templ layout() {
	<!DOCTYPE html>
	<html>
		<head>
			<title>Cat Facts</title>
			<link href="/tailwind.css" rel="stylesheet"/>
		</head>
		<body>
			<main>
				{ children... }
			</main>
		</body>
	</html>
}

templ Fact(fact string, factNum int) {
	<li class="flex items-center gap-4">
		<span class="font-semibold">{ strconv.Itoa(factNum) }{ "." }</span>
		<span>
			{ fact }
		</span>
	</li>
}

templ Facts(facts []string) {
	<ul id="facts-list" class="w-full flex flex-col gap-2">
		for idx, fact := range facts {
			@Fact(fact, idx+1)
		}
	</ul>
}

templ Index(facts []string) {
	@layout() {
		<div class="p-8">
			<h1 class="mb-4 font-bold text-3xl">Cat Facts</h1>
			@Facts(facts)
		</div>
	}
}
```

Bastante simples, certo? Rodei o servidor pra ver se tava tudo certo:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h12hjankb4cdnv1is2uc.gif)

Por conta daquele atraso de 2 segundos que eu simulo antes de bater na API de fatos sobre gatos, fica bem claro aqui o quão ruim é a experiência de quem acessa o site. Nenhum conteúdo é carregado até que _todo_ o conteúdo tenha sido carregado. Péssimo. Péssimo. Especialmente porque temos elementos que poderiam ser enviados imediatamente enquanto os dados demorados são carregados. Por exemplo, temos um belíssimo título que diz **Cat Facts**. Isso poderia já aparecer de cara pra que a pessoa que acessou o site entenda que ele já carregou. Além disso, poderíamos ter uma mensagem ou uma animação que deixasse bem claro que ainda há informações sendo carregadas, mas que chegam já já.

## Implementando a funcionalidade de defer

Defer, em inglês, significa "adiar", "postergar". É exatamente isso que queremos: uma forma de enviar dados assim que eles estiverem disponíveis, sem que seja preciso esperar por aqueles que ainda estão sendo carregados. A lógica aqui é: envia o que tem; _adia_ o envio daquilo que ainda não tem.

A forma mais simples de fazer isso seria fazer com que o servidor enviasse o HTML da página duas vezes: na primeira, assim que a requisição chega, ele envia um HTML contendo tudo aquilo que não depende da resposta da API, ou seja, a estrutura da página, títulos, menus, etc. Na segunda, enviada após a resposta da API, o servidor envia tudo o que enviou na primeira e mais os fatos sobre gatos, agora contidos na página.

Pra isso, vamos modificar a função `factsHandler` e usar a interface [`http.Flusher`](https://pkg.go.dev/net/http#Flusher) para enviar as duas diferentes versões da página assim que cada uma estiver pronta.

"Flush" em inglês significa "descarga". Aqui, acho que dá pra entender como "despachar", "enviar". Se você parar pra pensar, é o que fazem as descargas das
sanitas no final das contas. `http.Flusher` pega aquilo que está escrito no `http.ResponseWriter` e envia para o cliente sem encerrar a conexão.

Depois de alterar a função `factsHandler`, ela ficou assim:

```go
func factsHandler(w http.ResponseWriter, r *http.Request) {
	flusher, canFlush := w.(http.Flusher)
	ctx := context.Background()

	if canFlush {
		// Se for possível usar o flusher, a gente logo de cara já renderiza o
        // componente Index, passando nil como argumento, já que ainda não
        // temos nenhum fato sobre gato pra exibir.
		components.Index(nil).Render(ctx, w)
		flusher.Flush()
	}

	catFacts, err := getFacts()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	var facts []string
	for _, catFact := range catFacts {
		facts = append(facts, catFact.Fact)
	}

	// Renderiza o `Index`, agora com os fatos.
	components.Index(facts).Render(ctx, w)

}
```

Nem toda instância de `http.ResponseWriter` implementa a interface `http.Flusher`, então é necessário checar se a implementação está disponível antes de tentar invocar `flusher.Flush()`. Se não houver uma implementação de `http.Flusher`, o código prossegue normalmente, e o cliente só vai receber a resposta uma vez que os dados da API estejam carregados. Aqui, que estou usando o pacote `net/http`, eu sei que `http.ResponseWriter` implementa `http.Flusher`. Mas se você estiver usando alguma biblioteca de terceiros, saiba que é possível que o `http.ResponseWriter` disponibilizado por ela pode não implementar.

Ok, já sabemos como fazer pra enviar HTML em partes. Mas agora precisamos alterar o componente `Index` pra conseguir lidar com a renderização inicial, quando ainda não temos os fatos sobre gatos. Pra isso, basta adicionar um `if` no componente que renderiza a lista de fatos sobre gatos:

```go
templ Facts(facts []string) {
	<ul id="facts-list" class="w-full flex flex-col gap-2">
		if facts != nil {
			for idx, fact := range facts {
				@Fact(fact, idx+1)
			}
		}
	</ul>
}
```

Maravilha! Agora, quando acessamos `http://localhost:3000/`, é possível ver que o título da página é exibido imediatamente, mas a página continua carregando e, após dois segundos, a lista com os fatos sobre gatos é exibida.

![Lista de fatos é carregada, mas ainda temos um problema...](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tkmnxxfy1fyz3ptx3h8p.gif)

Porém, temos aqui ainda dois problemas:

1. Quando o servidor responde pela segunda vez enviando a lista de fatos, todo o conteúdo do componente `Index` é anexado ao HTML que já havia sido renderizado, resultando em uma duplição do conteúdo da página.
2. Essa não é a maneira mais eficiente de enviar os dados. Não faz muito sentindo enviar novamente elementos que já foram enviados no primeiro `flusher.Flush()`. O ideal é que a cada `flusher.Flush()`, a gente envie somente conteúdo inédito. Dessa forma, garantimos que o mínimo possível de dados seja transmitido através da rede.

### Preparando o frontend para receber flushes subsequentes

Precisamos mudar a estratégia para que o front receba primeiro o conteúdo estático inicial e, depois, somente o conteúdo do componente `Facts`. Vamos a isto. A primeira coisa que vou fazer é criar um componente chamado `LazyComponent`. Esse
componente, quando instanciado com um `fallback` não-nulo, vai renderizar uma `<div>` com o atributo `data-lazy-id` e contendo o componente `@fallback` recebido como parâmetro. Isso é útil pra exibir alguma mensagem ou animação de carregamento enquanto o conteúdo real desse componente não é exibido.

Porém, se for instanciado sem `fallback`, o componente simplesmente exibe o conteúdo de `children`.

```go
templ LazyComponent(lazyID string, fallback templ.Component) {
	if fallback == nil {
		<div>
			{ children... }
		</div>
	} else {
		<div
			data-lazy-id={ lazyID }
		>
			@fallback
		</div>
	}
}
```

Agora, em `Facts` precisamos verificar se a lista de fatos é `nil`. Se for, renderiza o `LazyComponent` exibindo a mensagem de carregamento (passando ela como `fallback`). Caso contrário, exibe a lista de fatos.

```go
templ FactsLoading() {
	<span>Carregando...</span>
}

templ Facts(facts []string) {
	if facts == nil {
		@LazyComponent("facts", FactsLoading())
	} else {
		@LazyComponent("facts", nil) {
			<ul id="facts-list" class="w-full flex flex-col gap-2">
				if facts != nil {
					for idx, fact := range facts {
						@Fact(fact, idx+1)
					}
				}
			</ul>
		}
	}
}
```

Ao acessar `http://localhost:3000/`, dá pra ver que o conteúdo da página não é mais duplicado! Além disso, agora conseguimos ver uma mensagem informando que os fatos estão sendo carregados. Como `Index` e `Facts` são renderizados separadamente, o servidor não envia mais conteúdo duplicado.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l7fi0qnrbblw1onsnsw0.gif)

Porém, esse ainda não é o comportamento ideal. Ainda precisamos fazer com que:

1. A mensagem de carregamento seja escondida quando a lista de fatos for exibida.
2. A lista de fatos seja renderizada _dentro_ de `<div data-lazy-id="facts">`. Atualmente ela é simplesmente anexada ao `<body>`:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sowy46dy7aj96t5qjfz9.png)

Pra resolver isso, vamos precisar usar um pouco de JavaScript. Vou começar criando um script que vai ser carregado na `<head>` da página. Para isso, vou criar um arquivo em `public/js/lazy-component.js`. Antes de implementar o script, preciso rotear esse arquivo lá em `main.go`, de modo que o browser vai conseguir baixá-lo:

```go
func main() {
	http.HandleFunc("/", factsHandler)
	http.HandleFunc(
		"/tailwind.css",
		func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "text/css")
			http.ServeFile(w, r, "public/css/tailwind.css")
		},
	)
    // Isto é necessário para que o browser possa baixar o script.
	http.HandleFunc(
		"/lazy-component.js",
		func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "text/css")
			http.ServeFile(w, r, "public/js/lazy-component.js")
		},
	)
	http.ListenAndServe(":3000", nil)
}
```

Outra coisa que é preciso fazer antes de implementar `lazy-component.js` é carregar o script dentro de `<head>`. Para isso, basta alterar o componente `layout`:

```html
templ layout() {
<!doctype html>
<html>
  <head>
    <title>Cat Facts</title>
    <link href="/tailwind.css" rel="stylesheet" />
    <!-- Carrega o script -->
    <script src="/lazy-component.js"></script>
  </head>
  <body>
    <main>{ children... }</main>
  </body>
</html>
}
```

Tudo certo, agora já podemos implementar o script:

```javascript
window.loadLazyComponent = function loadLazyComponent(lazyId, elementId) {
  const suspenseEl = document.querySelector(`div[data-lazy-id="${lazyId}"]`);
  const lazyEl = document.querySelector(`#${elementId}`);

  if (suspenseEl == null || lazyEl == null) {
    return;
  }

  suspenseEl.innerHTML = lazyEl.innerHTML;
  lazyEl.remove();
};
```

Este script é bastante simples. Ele cria uma propriedade global em `window` cujo valor é uma função chamada `loadLazyComponent`. Um passo-a-passo do que essa função faz:

1. Recebe `lazyId` e `elementId` como argumentos.
2. Busca no DOM um elemento cujo atributo `data-lazy-id` é igual ao valor de `lazyId` recebido. Cria uma referência a esse elemento na variável `suspenseEl`.
3. Busca no DOM um elemento cujo `id` seja igual ao `elementId` recebido. Cria uma referência a esse elemento na variável `lazyEl`.
4. Verifica se qualquer um dos elementos buscados não foi encontrado, ou seja, se é `null`. Se for, encerra a execução da função.
5. Substitui todo o conteúdo do elemento `suspenseEl` pelo conteúdo do elemento `lazyEl`.
6. Remove `lazyEl` do DOM.

Ok, a gente já sabe o que é `data-lazy-id`. Mas e `elementId`? Isso a gente ainda não implementou. Vamos voltar lá em `LazyComponent` e alterá-lo pra que ele consiga começar a usar a função `loadLazyComponent`.

```go
// Utilidade do Templ que permite que a gente escreva JavaScript e consiga
// incluir esse script como se fosse um componente dentro de outro componente
// Templ.
script loadLazyComponent(lazyID string, elementID string) {
    // Aqui a gente simplesmente faz uma chamada à função que foi criada lá em
    // `/public/js/lazy-component.js`.
    window.loadLazyComponent(lazyID, elementID)
}

templ LazyComponent(lazyID string, fallback templ.Component) {
	if fallback == nil {
        // Um id gerado aleatoriamente.
		{{ elementID, err := generateLazyElementID() }}
		if err != nil {
			<div>Error</div>
		}
        // Agora, a div que envelopa children pode ser identificada pelo
        // elementID.
		<div id={ elementID }>
			{ children... }
		</div>
		@loadLazyComponent(lazyID, elementID)
	} else {
		<div
			data-lazy-id={ lazyID }
		>
			@fallback
		</div>
	}
}
```

Só pra recapitular rapidamente pra ficar clara a ordem em que as coisas
acontecem:

1. Browser acessa "/".
2. Servidor responde imediatamente com o conteúdo estático. Nesse conteúdo, o script `lazy-component.js` é executado e cria a função `loadLazyComponent`. Além disso, nessa primeira resposta, o componente `Facts` recebe apenas `nil` como parâmetro, o que faz com que `LazyComponent` seja renderizado com um `fallback`, o que, por sua vez, faz com que apenas a mensagem de carregamento seja exibida.
3. Servidor faz uma chamada à API de fatos sobre gatos.
4. Servidor dá a sua segunda resposta, com o conteúdo dos fatos sobre gatos. Agora, o componente `Facts` recebe uma lista de fatos e, por isso, renderiza `LazyComponent` sem `fallback`. Sem `fallback`, `LazyComponent` exibe o conteúdo recebido em `children` e chama a função JavaScript `window.loadLazyComponent`, passando `lazyID` e `elementID`.
5. `window.loadLazyComponent` substitui a mensagem de carregamento pela lista de fatos.

E este é o resultado final:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hipiux7nznqhghmyz81x.gif)


Dessa forma, com pouco código e, principalmente, com apenas uma pitada de _JavaScript baunilha_ a gente consegue melhorar muito a experiência do usuário. Tradicionalmente em sites estáticos ou renderizados no servidor, isso seria feito transferindo a lógica da chamada à API de fatos sobre gatos pro cliente. O browser carregaria o conteúdo estático e ficaria a cargo de algum JavaScript exibir uma mensagem de carregamento, chamar a API e, então, exibir a resposta. E qual é o problema em fazer dessa forma? O problema é que essa estratégia impede que coisas que não dependem umas das outras sejam paralelizadas. Por que deveríamos esperar o navegador baixar e interpretar o HTML da página, parsear e executar o JavaScript pra, só então, fazer a chamada à API? Enquanto o browser tá ocupado com as coisas de browser, o servidor pode se ocupar das coisas de servidor.

Apenas uma observação sobre esta implementação: ela tem um pequeno problema, mas decidi não endereçá-lo neste post porque acho que foge um pouco do escopo do que eu queria demonstrar. O problema é que, se a segunda resposta do servidor for enviada antes que o navegador termine de fazer o parse e a execução do JavaScript em `lazy-component.js`, a mensagem de carregamento não vai ser removida, já que a função que o faz ainda não vai existir em `window`. Deixo Aí como exercício pra você pensar. Como você resolveria esse problema?