Programação Por Equipe DevBras

Capa do artigo: Div que se atualiza sozinha: polling com erro visível, sem empilhar requisições

Div que se atualiza sozinha: polling com erro visível, sem empilhar requisições

Como atualizar um pedaço da página a cada tantos segundos sem mentir: estado de carregando e falha, horário da última atualização, pausa na aba oculta e nada de pedidos sobrepostos.

O painel da expedição mostra os pedidos prontos para envio e se atualiza a cada 10 segundos. Numa terça, o Wi-Fi do galpão caiu por 20 minutos. A lista continuou na tela, parada, com cara de atual. A equipe despachou com base num retrato velho e dois pedidos cancelados saíram no caminhão. A div não quebrou. Ela mentiu.

A resposta curta: uma div que se atualiza sozinha precisa dizer quando foi atualizada pela última vez e avisar quando falhou. Use fetch com setTimeout encadeado (não setInterval), um tempo limite por requisição, pausa quando a aba fica oculta, espera maior depois de erro e, se a vista tiver filtro, guarde o filtro na URL.

Por que a atualização automática mente

O tutorial clássico faz isto: setInterval chama um load() do jQuery a cada N segundos e troca o conteúdo da div. Funciona no dia da demonstração. Os problemas aparecem com rede ruim.

  • Falha silenciosa: se a requisição falha, nada acontece. O conteúdo antigo fica, e não há diferença visual entre "atualizado agora" e "atualizado há 20 minutos".
  • Requisições empilhadas: setInterval dispara no horário, não quando a anterior termina. Se o servidor demora 15 segundos e o intervalo é 10, as requisições se acumulam e as respostas podem chegar fora de ordem. A mais velha pode sobrescrever a mais nova.
  • Carga sem ninguém olhando: trinta abas esquecidas abertas no escritório fazem trinta consultas a cada 10 segundos para ninguém.
  • Estado perdido: se a pessoa filtrou "só transportadora X" e apertou F5, o filtro some, porque ele vivia numa variável JavaScript, não no endereço.

Como montar o polling do jeito certo

  1. Faça o servidor entregar o conteúdo inicial já no HTML. A página tem que mostrar algo útil mesmo antes do primeiro ciclo de script.
  2. Agende a próxima atualização só depois que a atual terminar, com setTimeout.
  3. Dê a cada requisição um tempo limite com AbortController.
  4. Mostre na tela o horário da última atualização bem-sucedida.
  5. Em falha, mantenha os dados antigos, mas marque como desatualizados e aumente o intervalo.
  6. Pause quando document.visibilityState for hidden e atualize na hora quando a aba voltar.

O HTML do painel:

<section aria-labelledby="titulo-prontos">
  <h2 id="titulo-prontos">Prontos para envio</h2>
  <p id="status-painel" role="status">Atualizado às 14:02</p>
  <div id="lista-prontos">
    <!-- lista gerada pelo servidor na primeira carga -->
  </div>
</section>

E o script, sem biblioteca:

const lista = document.getElementById('lista-prontos');
const status = document.getElementById('status-painel');
const INTERVALO_NORMAL = 10000;
const INTERVALO_MAXIMO = 120000;
let intervalo = INTERVALO_NORMAL;
let agendado = null;
let emAndamento = false;
let ultimaAtualizacao = status.textContent.replace('Atualizado às ', '');

function horaAgora() {
  return new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}

async function atualizar() {
  if (emAndamento) return;
  emAndamento = true;
  const controle = new AbortController();
  const limite = setTimeout(function () { controle.abort(); }, 8000);
  try {
    const params = new URLSearchParams(location.search);
    const resposta = await fetch('/expedicao/prontos?' + params.toString(), {
      signal: controle.signal,
      headers: { 'Accept': 'text/html' }
    });
    if (!resposta.ok) throw new Error('HTTP ' + resposta.status);
    lista.innerHTML = await resposta.text();
    ultimaAtualizacao = horaAgora();
    status.textContent = 'Atualizado às ' + ultimaAtualizacao;
    intervalo = INTERVALO_NORMAL;
  } catch (erro) {
    status.textContent = 'Falha ao atualizar. Os dados são de ' +
      ultimaAtualizacao + ' e podem estar desatualizados.';
    intervalo = Math.min(intervalo * 2, INTERVALO_MAXIMO);
  } finally {
    clearTimeout(limite);
    emAndamento = false;
    agendar();
  }
}

function agendar() {
  clearTimeout(agendado);
  if (document.visibilityState === 'hidden') return;
  agendado = setTimeout(atualizar, intervalo);
}

document.addEventListener('visibilitychange', function () {
  if (document.visibilityState === 'visible') atualizar();
  else clearTimeout(agendado);
});

agendar();

Repare nos detalhes. A flag emAndamento impede duas requisições ao mesmo tempo, mesmo que a aba volte ao foco no meio de um ciclo. O setTimeout de 8 segundos aborta a requisição pendurada, e o erro cai no catch como qualquer outra falha. Depois de cada erro o intervalo dobra até 2 minutos, para não martelar um servidor que já está com problema.

O parágrafo com role="status" é anunciado por leitores de tela quando muda, sem roubar o foco. Se a cada 10 segundos o anúncio "Atualizado às" incomodar, mostre o horário em um elemento comum e reserve a região de status só para a mensagem de falha.

O conteúdo que entra na div

O exemplo usa innerHTML porque a resposta vem do seu próprio servidor, gerada a partir de um template que escapa os dados. Se a resposta for JSON, monte os elementos com textContent. Nunca injete HTML vindo de terceiro sem sanear: um nome de cliente com <img onerror> vira script rodando no painel de toda a equipe.

Filtro na URL: o estado que sobrevive ao F5

O painel tem um seletor de transportadora. Se a escolha só mudar uma variável, o F5 e o link enviado para o colega perdem o filtro. Grave o filtro na URL com history.replaceState e leia de lá a cada ciclo, como o script já faz com location.search:

document.getElementById('filtro-transportadora').addEventListener('change', function (e) {
  const params = new URLSearchParams(location.search);
  if (e.target.value) params.set('transportadora', e.target.value);
  else params.delete('transportadora');
  history.replaceState(null, '', '?' + params.toString());
  atualizar();
});

Aqui replaceState basta, porque trocar o filtro de um painel não é navegação. Quando a troca é de verdade uma página nova, como abrir outra seção, use pushState e trate o popstate. Esse caso tem um guia próprio: trocar o miolo da página sem quebrar o botão voltar. Para formulários enviados sem recarregar, veja enviar formulário sem recarregar, sem perder o erro.

Quando polling não é a ferramenta

Polling a cada 10 segundos é simples e suficiente para painéis internos. Se a atualização precisa ser imediata, como chat ou leilão, ou se milhares de pessoas olham a mesma tela, considere Server-Sent Events ou WebSocket, em que o servidor avisa quando muda. Se o dado muda uma vez por hora, atualizar a cada 10 segundos é desperdício: aumente o intervalo ou ofereça um botão "atualizar".

Erros comuns

  • Usar setInterval com requisição lenta: os pedidos se sobrepõem e uma resposta antiga pode sobrescrever a mais nova.
  • Ignorar a falha: a tela continua mostrando dados velhos sem aviso, e alguém toma decisão com eles.
  • Não ter tempo limite: uma requisição pendurada trava o ciclo inteiro, e o painel para de atualizar sem erro nenhum.
  • Atualizar com a aba oculta: abas esquecidas geram carga constante no servidor para ninguém ver.
  • Insistir no mesmo ritmo depois do erro: com o servidor já sobrecarregado, cada painel aberto piora a situação.
  • Guardar o filtro só em variável: o F5 e o link compartilhado perdem o que a pessoa escolheu.

Checklist

  • A primeira carga mostra o conteúdo sem depender do script.
  • A próxima atualização só é agendada depois que a atual termina.
  • Cada requisição tem tempo limite.
  • A tela mostra o horário da última atualização bem-sucedida.
  • Uma falha deixa aviso visível e aumenta o intervalo.
  • A atualização pausa com a aba oculta e roda na hora quando ela volta.
  • O filtro escolhido sobrevive a um F5.

Abra o seu painel, desligue a rede no DevTools por um minuto e olhe para a tela: alguém que chegasse agora saberia que aqueles dados estão parados?

Avalie este artigo

Referência: https://webvisao.wordpress.com/2012/06/17/tutorial-como-fazer-uma-div-se-atualizar-sem-refresh-ou-reload-da-pagina-completa/