Ir para o conteúdo
Trilha
Capítulos

Aprender

O teto

Onde a troca de pedaço deixa de ser a ferramenta certa, e como atravessar sem virar SPA.

Todo framework tem um teto. Os que merecem confiança dizem onde ele fica.

O modelo do Trilha — o servidor renderiza HTML, um link ou um formulário pede um pedaço, o navegador troca aquele elemento — cobre a maior parte de uma aplicação. Não toda. Esta página é sobre a parte que ele não cobre, como reconhecê-la antes de você estar três dias brigando com ela, e o que fazer quando chegar lá.

O que a troca de pedaço cobre#

Se a tela muda porque algo aconteceu no servidor, o fragmento é a resposta inteira: uma lista filtrada, um formulário que valida, uma linha apagada, um painel que abre já com dados, uma tabela que pagina. O estado mora em um lugar só, a URL continua significando o que diz, e não há store para manter em dia com o DOM.

Esses são os 90%, e vale defendê-los: uma troca não tem etapa de hidratação, então não tem bug de hidratação.

Onde o teto fica#

O teto não é sobre o quanto a tela parece complicada. É sobre quem é dono da verdade enquanto a pessoa interage.

Você está acima do teto quando, mesmo por um instante, só o navegador sabe o que está na tela:

  • Um gesto contínuo. Arrastar para reordenar, redimensionar um painel, desenhar num canvas, recortar uma imagem. Entre apertar e soltar o ponteiro não cabe ida ao servidor.
  • Estado que precisa sobreviver a uma troca. Um comentário digitado pela metade, a posição de rolagem dentro de uma lista virtualizada, um combobox aberto com filtro digitado.
  • Duas pessoas ao mesmo tempo. Edição colaborativa, cursores ao vivo, qualquer coisa em que a mudança de outro cliente precise chegar sem pisar no que você está fazendo.
  • Um ritmo que a rede não acompanha. Sessenta quadros por segundo, áudio, laço de jogo.

Um teste curto: se eu desligasse a rede agora, a tela ainda teria que responder? Se sim, aquela parte é do cliente.

Atravessando: uma ilha#

Uma ilha é um pedaço da página que um módulo JavaScript assume, com props que o servidor calculou e o HTML que o servidor já renderizou como recuo. Tudo em volta continua vindo do servidor.

c.Island("/ilha-ordem.js", map[string]any{"ordem": slugs},
    h.Input(h.Type("hidden"), h.Name("ordem"), h.Value(strings.Join(slugs, ","))),
    h.Ol(h.Data("lista", ""), linhas...),
)
// public/ilha-ordem.js — um módulo, sem bundler, sem etapa de build
export default function (el, props) {
  const lista = el.querySelector("[data-lista]");
  const campo = el.querySelector('input[name="ordem"]');
  // o arrastar acontece aqui, no navegador, que é onde ele tem que acontecer
  const grava = () => { campo.value = linhas().map((li) => li.dataset.slug).join(","); };
}

O exemplo inteiro — o arrastar, o caminho pelo teclado, o handler que salva — está em examples/blog, rota /blog/ordem.

As regras que impedem a ilha de virar uma SPA#

O servidor continua dono dos dados. A ilha escreve num campo do formulário e para por aí. Quem salva a ordem é o mesmo POST → redirect → GET do resto do app — não um fetch privado para um endpoint privado com um formato privado. No instante em que uma ilha começa a persistir por conta própria, você tem duas fontes de verdade e um bug de sincronização à espera.

As props são a verdade inicial. O servidor renderiza a ordem; a ilha a recebe. Um módulo que carrega tarde, ou que não carrega, não pode deixar a página mostrando algo que nunca foi verdade.

O recuo é HTML de verdade, não um spinner. Os filhos de c.Island são a cara da página sem o módulo. Se aquilo é uma caixa vazia, a ilha não é aprimoramento: é exigência com passos a mais.

Não tire o teclado. Arrastar não é alcançável pelo teclado. O exemplo mantém botões ↑ ↓ que enviam um passo pelo mesmo handler, e a ilha não mexe neles. Ilha que troca acessibilidade por acabamento é piora.

Uma ilha, um trabalho. Três ilhas que cuidam de uma coisa pequena cada são mais fáceis de apagar do que uma que cuida da tela inteira.

Ilha e troca compõem#

Uma ilha monta ao entrar no documento, tendo vindo com a página inteira ou dentro de um fragmento trocado. Você pode trocar um painel que contém uma ilha e ela começa; pode trocar o painel fora e o módulo vai junto.

Antes da 0.40.0 isso só valia quando a página já tinha alguma ilha. A ilha que chegava num fragmento numa página sem nenhuma ficava parada e muda — o loader vinha junto como <script>, e o DOM não executa script inserido assim.

Quando nem a ilha basta#

Seja honesto com o formato do seu app. Se a maioria das telas está acima do teto — um editor de diagramas, uma planilha, uma estação de áudio —, então você não está escrevendo um app com algumas partes interativas: está escrevendo uma aplicação de cliente, e um framework de cliente vai servir melhor que um monte de ilhas.

O Trilha continua útil ali: sirva a casca, a autenticação, as páginas de configuração e a API pelo Trilha, e dê ao app de cliente a rota dele. O que não vale é fazer uma ilha crescer até virar um framework que ninguém escolheu.

O que isso custa, sem maquiagem#

Uma ilha é JavaScript seu: sem tipos compartilhados com o servidor, sem compilador conferindo que props.ordem existe, e com um segundo lugar onde um bug pode morar. É o preço de atravessar o teto, e é por isso que vale nomear o teto — para você pagar esse preço nas telas que precisam, e não nas outras noventa.

Duas coisas diminuem o preço. O public/islands.d.ts, gravado pelo trilha gen, dá ao módulo os tipos das props, inclusive os que vêm de outros pacotes do módulo. E a ilha que passou de um arquivo — um jogo, um editor, um motor — pode ser escrita em TypeScript: ponha as fontes e o tsconfig.json delas em islands/, e o trilha islands build compila com o tsc do próprio projeto para public/, onde a saída é commitada. O trilha check reprova quando public/ não é o que islands/ compila. Continua sem bundler: cada .ts vira um .js, importado pelo caminho.

Desafio#

A ilha da ordem escreve a ordem inteira num campo escondido, então nada é salvo até a pessoa apertar o botão — e, se ela sair da página, o arrastar se perde. Faça a ordem salvar sozinha, um instante depois que o arrastar para, sem dar à ilha um endpoint próprio e sem quebrar o botão para quem não tem JavaScript.

Mostrar solução
let t;
const salvaSozinho = () => {
  clearTimeout(t);
  t = setTimeout(() => campo.form.requestSubmit(), 800);
};
lista.addEventListener("dragend", salvaSozinho);

O requestSubmit() envia o mesmo formulário, pelo mesmo handler, com o token de CSRF que já está nele — então não há segundo endpoint nem segundo formato para manter em dia. Ponha ui.Swap("lista") no formulário e a resposta volta como fragmento em vez de redirect, que é o que impede a página de pular debaixo do ponteiro.

O atraso importa pelo mesmo motivo que o limiar do indicador: o dragend dispara a cada solta, e quem reordena cinco linhas solta cinco vezes. Esperar a pessoa parar transforma cinco pedidos em um.